Inventory properties via Assembly schema

This commit implements Redfish Assembly schema.
This schema will be used to publish inventory data for FRUs which are
attached to a given Chassis and does not map to any specific schema
definition.

The properties which are published in this commit are LocationCode,
SparePartNumber, Model, SerialNumber and PartNumber.

One of the major use case to publish these properties via redfish is for
anyone to identify the inventory and its location in the system, which
in turn will help them in repair/replacement related to that FRU.

The validator has been executed on the change and no error has been
found.
As this has been tested on a development image some fields are empty
in the below pasted output for which warning was thrown by validator but
no errors.

Sample Output with [1]:
```
{
  "@odata.id": "/redfish/v1/Chassis/chassis/Assembly",
  "@odata.type": "#Assembly.v1_5_1.Assembly",
  "Assemblies": [
    {
      "@odata.id": "/redfish/v1/Chassis/chassis/Assembly#/Assemblies/0",
      "@odata.type": "#Assembly.v1_5_1.AssemblyData",
      "Location": {
        "PartLocation": {
          "ServiceLabel": "U78DA.ND0.1234567-D0"
        }
      },
      "Manufacturer": "",
      "MemberId": "0",
      "Model": "",
      "Name": "base_op_panel_blyth",
      "PartNumber": "",
      "SerialNumber": "",
      "Status": {
        "Health": "OK",
        "State": "Absent"
      }
    },
    {
      "@odata.id": "/redfish/v1/Chassis/chassis/Assembly#/Assemblies/1",
      "@odata.type": "#Assembly.v1_5_1.AssemblyData",
      "Location": {
        "PartLocation": {
          "ServiceLabel": "U78DA.ND0.1234567-D1"
        }
      },
      "Manufacturer": "",
      "MemberId": "1",
      "Model": "6B86",
      "Name": "lcd_op_panel_hill",
      "PartNumber": "PN12345",
      "SerialNumber": "YL6B86010000",
      "Status": {
        "Health": "OK",
        "State": "Enabled"
      }
    }
  ],
  "Assemblies@odata.count": 2,
  "Id": "Assembly",
  "Name": "Assembly Collection"
}
```

[1] https://gerrit.openbmc.org/c/openbmc/openbmc/+/83907

Change-Id: I2d462340fe1a0b0eb387697f0ff70fcafde3f8d9
Signed-off-by: Sunny Srivastava <sunnsr25@in.ibm.com>
Signed-off-by: Ninad Palsule <ninad@linux.ibm.com>
Signed-off-by: Myung Bae <myungbae@us.ibm.com>
diff --git a/docs/Redfish.md b/docs/Redfish.md
index 3b91880..4def670 100644
--- a/docs/Redfish.md
+++ b/docs/Redfish.md
@@ -462,6 +462,20 @@
 - SparePartNumber
 - Status
 
+#### /redfish/v1/Chassis/{ChassisId}/Assembly
+
+##### Assemblies
+
+- Assemblies
+- `Assemblies@odata.count`
+
+###### Assembly
+
+- Model
+- PartNumber
+- SerialNumber
+- SparePartNumber
+
 ### /redfish/v1/EventService/
 
 #### EventService
diff --git a/redfish-core/include/utils/asset_utils.hpp b/redfish-core/include/utils/asset_utils.hpp
index be97276..c24f1ad 100644
--- a/redfish-core/include/utils/asset_utils.hpp
+++ b/redfish-core/include/utils/asset_utils.hpp
@@ -28,7 +28,7 @@
     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
     const nlohmann::json::json_pointer& jsonKeyName,
     const dbus::utility::DBusPropertiesMap& assetList,
-    bool includeSparePartNumber = false)
+    bool includeSparePartNumber = false, bool includeManufacturer = true)
 {
     const std::string* manufacturer = nullptr;
     const std::string* model = nullptr;
@@ -48,7 +48,7 @@
 
     nlohmann::json& assetData = asyncResp->res.jsonValue[jsonKeyName];
 
-    if (manufacturer != nullptr)
+    if (includeManufacturer && manufacturer != nullptr)
     {
         assetData["Manufacturer"] = *manufacturer;
     }
@@ -72,15 +72,15 @@
     }
 }
 
-inline void getAssetInfo(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
-                         const std::string& serviceName,
-                         const std::string& dbusPath,
-                         const nlohmann::json::json_pointer& jsonKeyName,
-                         bool includeSparePartNumber = false)
+inline void getAssetInfo(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& serviceName, const std::string& dbusPath,
+    const nlohmann::json::json_pointer& jsonKeyName,
+    bool includeSparePartNumber = false, bool includeManufacturer = true)
 {
     dbus::utility::getAllProperties(
         serviceName, dbusPath, "xyz.openbmc_project.Inventory.Decorator.Asset",
-        [asyncResp, jsonKeyName, includeSparePartNumber](
+        [asyncResp, jsonKeyName, includeSparePartNumber, includeManufacturer](
             const boost::system::error_code& ec,
             const dbus::utility::DBusPropertiesMap& assetList) {
             if (ec)
@@ -94,7 +94,7 @@
                 return;
             }
             extractAssetInfo(asyncResp, jsonKeyName, assetList,
-                             includeSparePartNumber);
+                             includeSparePartNumber, includeManufacturer);
         });
 }
 
diff --git a/redfish-core/lib/assembly.hpp b/redfish-core/lib/assembly.hpp
new file mode 100644
index 0000000..4112a79
--- /dev/null
+++ b/redfish-core/lib/assembly.hpp
@@ -0,0 +1,316 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+
+#include "app.hpp"
+#include "async_resp.hpp"
+#include "dbus_singleton.hpp"
+#include "dbus_utility.hpp"
+#include "error_messages.hpp"
+#include "generated/enums/resource.hpp"
+#include "http_request.hpp"
+#include "http_response.hpp"
+#include "logging.hpp"
+#include "query.hpp"
+#include "registries/privilege_registry.hpp"
+#include "utils/assembly_utils.hpp"
+#include "utils/asset_utils.hpp"
+#include "utils/chassis_utils.hpp"
+#include "utils/dbus_utils.hpp"
+
+#include <boost/beast/http/verb.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/url/format.hpp>
+#include <nlohmann/json.hpp>
+#include <sdbusplus/asio/property.hpp>
+#include <sdbusplus/unpack_properties.hpp>
+
+#include <cstddef>
+#include <functional>
+#include <memory>
+#include <ranges>
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace redfish
+{
+
+/**
+ * @brief Get Location code for the given assembly.
+ * @param[in] asyncResp - Shared pointer for asynchronous calls.
+ * @param[in] serviceName - Service in which the assembly is hosted.
+ * @param[in] assembly - Assembly object.
+ * @param[in] assemblyJsonPtr - json-keyname on the assembly list output.
+ * @return None.
+ */
+inline void getAssemblyLocationCode(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& serviceName, const std::string& assembly,
+    const nlohmann::json::json_pointer& assemblyJsonPtr)
+{
+    sdbusplus::asio::getProperty<std::string>(
+        *crow::connections::systemBus, serviceName, assembly,
+        "xyz.openbmc_project.Inventory.Decorator.LocationCode", "LocationCode",
+        [asyncResp, assembly, assemblyJsonPtr](
+            const boost::system::error_code& ec, const std::string& value) {
+            if (ec)
+            {
+                if (ec.value() != EBADR)
+                {
+                    BMCWEB_LOG_ERROR("DBUS response error: {} for assembly {}",
+                                     ec.value(), assembly);
+                    messages::internalError(asyncResp->res);
+                }
+                return;
+            }
+
+            asyncResp->res.jsonValue[assemblyJsonPtr]["Location"]
+                                    ["PartLocation"]["ServiceLabel"] = value;
+        });
+}
+
+inline void getAssemblyState(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const auto& serviceName, const auto& assembly,
+    const nlohmann::json::json_pointer& assemblyJsonPtr)
+{
+    asyncResp->res.jsonValue[assemblyJsonPtr]["Status"]["State"] =
+        resource::State::Enabled;
+
+    dbus::utility::getProperty<bool>(
+        serviceName, assembly, "xyz.openbmc_project.Inventory.Item", "Present",
+        [asyncResp, assemblyJsonPtr,
+         assembly](const boost::system::error_code& ec, const bool value) {
+            if (ec)
+            {
+                if (ec.value() != EBADR)
+                {
+                    BMCWEB_LOG_ERROR("DBUS response error: {}", ec.value());
+                    messages::internalError(asyncResp->res);
+                }
+                return;
+            }
+
+            if (!value)
+            {
+                asyncResp->res.jsonValue[assemblyJsonPtr]["Status"]["State"] =
+                    resource::State::Absent;
+            }
+        });
+}
+
+void getAssemblyHealth(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                       const auto& serviceName, const auto& assembly,
+                       const nlohmann::json::json_pointer& assemblyJsonPtr)
+{
+    asyncResp->res.jsonValue[assemblyJsonPtr]["Status"]["Health"] =
+        resource::Health::OK;
+
+    dbus::utility::getProperty<bool>(
+        serviceName, assembly,
+        "xyz.openbmc_project.State.Decorator.OperationalStatus", "Functional",
+        [asyncResp, assemblyJsonPtr](const boost::system::error_code& ec,
+                                     bool functional) {
+            if (ec)
+            {
+                if (ec.value() != EBADR)
+                {
+                    BMCWEB_LOG_ERROR("DBUS response error {}", ec.value());
+                    messages::internalError(asyncResp->res);
+                }
+                return;
+            }
+
+            if (!functional)
+            {
+                asyncResp->res.jsonValue[assemblyJsonPtr]["Status"]["Health"] =
+                    resource::Health::Critical;
+            }
+        });
+}
+
+inline void afterGetDbusObject(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& assembly,
+    const nlohmann::json::json_pointer& assemblyJsonPtr,
+    const boost::system::error_code& ec,
+    const dbus::utility::MapperGetObject& object)
+{
+    if (ec)
+    {
+        BMCWEB_LOG_ERROR("DBUS response error : {} for assembly {}", ec.value(),
+                         assembly);
+        messages::internalError(asyncResp->res);
+        return;
+    }
+
+    for (const auto& [serviceName, interfaceList] : object)
+    {
+        for (const auto& interface : interfaceList)
+        {
+            if (interface == "xyz.openbmc_project.Inventory.Decorator.Asset")
+            {
+                asset_utils::getAssetInfo(asyncResp, serviceName, assembly,
+                                          assemblyJsonPtr, true, false);
+            }
+            else if (interface ==
+                     "xyz.openbmc_project.Inventory.Decorator.LocationCode")
+            {
+                getAssemblyLocationCode(asyncResp, serviceName, assembly,
+                                        assemblyJsonPtr);
+            }
+            else if (interface == "xyz.openbmc_project.Inventory.Item")
+            {
+                getAssemblyState(asyncResp, serviceName, assembly,
+                                 assemblyJsonPtr);
+            }
+            else if (interface ==
+                     "xyz.openbmc_project.State.Decorator.OperationalStatus")
+            {
+                getAssemblyHealth(asyncResp, serviceName, assembly,
+                                  assemblyJsonPtr);
+            }
+        }
+    }
+}
+
+/**
+ * @brief Get properties for the assemblies associated to given chassis
+ * @param[in] asyncResp - Shared pointer for asynchronous calls.
+ * @param[in] chassisId - Chassis the assemblies are associated with.
+ * @param[in] assemblies - list of all the assemblies associated with the
+ * chassis.
+ * @return None.
+ */
+inline void getAssemblyProperties(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& chassisId, const std::vector<std::string>& assemblies)
+{
+    BMCWEB_LOG_DEBUG("Get properties for assembly associated");
+
+    std::size_t assemblyIndex = 0;
+    for (const std::string& assembly : assemblies)
+    {
+        nlohmann::json::object_t item;
+        item["@odata.type"] = "#Assembly.v1_5_1.AssemblyData";
+        item["@odata.id"] = boost::urls::format(
+            "/redfish/v1/Chassis/{}/Assembly#/Assemblies/{}", chassisId,
+            std::to_string(assemblyIndex));
+        item["MemberId"] = std::to_string(assemblyIndex);
+        item["Name"] = sdbusplus::message::object_path(assembly).filename();
+
+        asyncResp->res.jsonValue["Assemblies"].emplace_back(item);
+
+        nlohmann::json::json_pointer assemblyJsonPtr(
+            "/Assemblies/" + std::to_string(assemblyIndex));
+
+        dbus::utility::getDbusObject(
+            assembly, assemblyInterfaces,
+            std::bind_front(afterGetDbusObject, asyncResp, assembly,
+                            assemblyJsonPtr));
+
+        nlohmann::json& assemblyArray = asyncResp->res.jsonValue["Assemblies"];
+        asyncResp->res.jsonValue["Assemblies@odata.count"] =
+            assemblyArray.size();
+
+        assemblyIndex++;
+    }
+}
+
+inline void afterHandleChassisAssemblyGet(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& chassisID, const boost::system::error_code& ec,
+    const std::vector<std::string>& assemblyList)
+{
+    if (ec)
+    {
+        BMCWEB_LOG_WARNING("Chassis {} not found", chassisID);
+        messages::resourceNotFound(asyncResp->res, "Chassis", chassisID);
+        return;
+    }
+
+    asyncResp->res.addHeader(
+        boost::beast::http::field::link,
+        "</redfish/v1/JsonSchemas/Assembly/Assembly.json>; rel=describedby");
+
+    asyncResp->res.jsonValue["@odata.type"] = "#Assembly.v1_5_1.Assembly";
+    asyncResp->res.jsonValue["@odata.id"] =
+        boost::urls::format("/redfish/v1/Chassis/{}/Assembly", chassisID);
+    asyncResp->res.jsonValue["Name"] = "Assembly Collection";
+    asyncResp->res.jsonValue["Id"] = "Assembly";
+
+    asyncResp->res.jsonValue["Assemblies"] = nlohmann::json::array();
+    asyncResp->res.jsonValue["Assemblies@odata.count"] = 0;
+
+    if (!assemblyList.empty())
+    {
+        getAssemblyProperties(asyncResp, chassisID, assemblyList);
+    }
+}
+
+/**
+ * @param[in] asyncResp - Shared pointer for asynchronous calls.
+ * @param[in] chassisID - Chassis to which the assemblies are
+ * associated.
+ *
+ * @return None.
+ */
+inline void handleChassisAssemblyGet(
+    App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& chassisID)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+
+    BMCWEB_LOG_DEBUG("Get chassis Assembly");
+    assembly_utils::getChassisAssembly(
+        asyncResp, chassisID,
+        std::bind_front(afterHandleChassisAssemblyGet, asyncResp, chassisID));
+}
+
+inline void handleChassisAssemblyHead(
+    crow::App& app, const crow::Request& req,
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& chassisID)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+
+    assembly_utils::getChassisAssembly(
+        asyncResp, chassisID,
+        [asyncResp,
+         chassisID](const boost::system::error_code& ec,
+                    const std::vector<std::string>& /*assemblyList*/) {
+            if (ec)
+            {
+                BMCWEB_LOG_WARNING("Chassis {} not found", chassisID);
+                messages::resourceNotFound(asyncResp->res, "Chassis",
+                                           chassisID);
+                return;
+            }
+            asyncResp->res.addHeader(
+                boost::beast::http::field::link,
+                "</redfish/v1/JsonSchemas/Assembly.json>; rel=describedby");
+        });
+}
+
+inline void requestRoutesAssembly(App& app)
+{
+    BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/Assembly/")
+        .privileges(redfish::privileges::headAssembly)
+        .methods(boost::beast::http::verb::head)(
+            std::bind_front(handleChassisAssemblyHead, std::ref(app)));
+
+    BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/Assembly/")
+        .privileges(redfish::privileges::getAssembly)
+        .methods(boost::beast::http::verb::get)(
+            std::bind_front(handleChassisAssemblyGet, std::ref(app)));
+}
+
+} // namespace redfish
diff --git a/redfish-core/lib/chassis.hpp b/redfish-core/lib/chassis.hpp
index afef198..5467f58 100644
--- a/redfish-core/lib/chassis.hpp
+++ b/redfish-core/lib/chassis.hpp
@@ -446,6 +446,10 @@
             boost::urls::format("/redfish/v1/Chassis/{}/EnvironmentMetrics",
                                 chassisId);
     }
+
+    asyncResp->res.jsonValue["Assembly"]["@odata.id"] =
+        boost::urls::format("/redfish/v1/Chassis/{}/Assembly", chassisId);
+
     // SensorCollection
     asyncResp->res.jsonValue["Sensors"]["@odata.id"] =
         boost::urls::format("/redfish/v1/Chassis/{}/Sensors", chassisId);
diff --git a/redfish-core/src/redfish.cpp b/redfish-core/src/redfish.cpp
index 988939f..a2801f1 100644
--- a/redfish-core/src/redfish.cpp
+++ b/redfish-core/src/redfish.cpp
@@ -7,6 +7,7 @@
 #include "account_service.hpp"
 #include "aggregation_service.hpp"
 #include "app.hpp"
+#include "assembly.hpp"
 #include "bios.hpp"
 #include "cable.hpp"
 #include "certificate_service.hpp"
@@ -66,6 +67,8 @@
     requestRoutesOdata(app);
 
     requestAccountServiceRoutes(app);
+    requestRoutesAssembly(app);
+
     if constexpr (BMCWEB_REDFISH_AGGREGATION)
     {
         requestRoutesAggregationService(app);