fw_update: InventoryManager: Add Downstream Devices Support

In DSP0267_1.1.0, a Firmware Device can supports one or more
Downstream Devices to perform get firmware version or firmware
update. Add discovery of Downstream Devices, query the Downstream
Devices Descriptor if the Firmware Device support it.

The code is developed based on DSP0267_1.1.0 Section 10.3
QueryDownstreamDevices command format, Section 10.4
QueryDownstreamIdentifiers command format.

Tested: Add unit tests for the new command handlers.

Change-Id: Iad28b898b5a0799b2b145d38958bba78e9719f4e
Signed-off-by: Unive Tien <unive.tien.wiwynn@gmail.com>
diff --git a/common/types.hpp b/common/types.hpp
index e3b10ee..42d626b 100644
--- a/common/types.hpp
+++ b/common/types.hpp
@@ -91,8 +91,12 @@
 using Descriptors =
     std::map<DescriptorType,
              std::variant<DescriptorData, VendorDefinedDescriptorInfo>>;
+using DownstreamDeviceIndex = uint16_t;
+using DownstreamDeviceInfo = std::tuple<DownstreamDeviceIndex, Descriptors>;
+using DownstreamDevices = std::vector<DownstreamDeviceInfo>;
 
 using DescriptorMap = std::unordered_map<eid, Descriptors>;
+using DownstreamDescriptorMap = std::unordered_map<eid, DownstreamDevices>;
 
 // Component information
 using CompClassification = uint16_t;
diff --git a/fw-update/inventory_manager.cpp b/fw-update/inventory_manager.cpp
index 43508a7..4101624 100644
--- a/fw-update/inventory_manager.cpp
+++ b/fw-update/inventory_manager.cpp
@@ -19,31 +19,47 @@
 {
     for (const auto& eid : eids)
     {
-        auto instanceId = instanceIdDb.next(eid);
-        Request requestMsg(
-            sizeof(pldm_msg_hdr) + PLDM_QUERY_DEVICE_IDENTIFIERS_REQ_BYTES);
-        auto request = reinterpret_cast<pldm_msg*>(requestMsg.data());
-        auto rc = encode_query_device_identifiers_req(
-            instanceId, PLDM_QUERY_DEVICE_IDENTIFIERS_REQ_BYTES, request);
-        if (rc)
+        try
         {
-            instanceIdDb.free(eid, instanceId);
-            error(
-                "Failed to encode query device identifiers req for endpoint ID '{EID}', response code '{RC}'",
-                "EID", eid, "RC", rc);
-            continue;
+            sendQueryDownstreamDevicesRequest(eid);
         }
+        catch (const std::exception& e)
+        {
+            error("Failed to discover FDs, EID={EID}, Error={ERROR}", "EID",
+                  eid, "ERROR", e.what());
+        }
+    }
+}
 
-        rc = handler.registerRequest(
-            eid, instanceId, PLDM_FWUP, PLDM_QUERY_DEVICE_IDENTIFIERS,
-            std::move(requestMsg),
-            std::bind_front(&InventoryManager::queryDeviceIdentifiers, this));
-        if (rc)
-        {
-            error(
-                "Failed to send query device identifiers request for endpoint ID '{EID}', response code '{RC}'",
-                "EID", eid, "RC", rc);
-        }
+void InventoryManager::sendQueryDeviceIdentifiersRequest(mctp_eid_t eid)
+{
+    auto instanceId = instanceIdDb.next(eid);
+    Request requestMsg(
+        sizeof(pldm_msg_hdr) + PLDM_QUERY_DEVICE_IDENTIFIERS_REQ_BYTES);
+    auto request = new (requestMsg.data()) pldm_msg;
+    auto rc = encode_query_device_identifiers_req(
+        instanceId, PLDM_QUERY_DEVICE_IDENTIFIERS_REQ_BYTES, request);
+    if (rc)
+    {
+        instanceIdDb.free(eid, instanceId);
+        error(
+            "Failed to encode query device identifiers request, EID={EID}, RC = {RC}",
+            "EID", eid, "RC", rc);
+        throw std::runtime_error(
+            "Failed to encode QueryDeviceIdentifiers request");
+    }
+
+    rc = handler.registerRequest(
+        eid, instanceId, PLDM_FWUP, PLDM_QUERY_DEVICE_IDENTIFIERS,
+        std::move(requestMsg),
+        std::bind_front(&InventoryManager::queryDeviceIdentifiers, this));
+    if (rc)
+    {
+        error(
+            "Failed to send query device identifiers request for endpoint ID '{EID}', response code '{RC}'",
+            "EID", eid, "RC", rc);
+        throw std::runtime_error(
+            "Failed to send QueryDeviceIdentifiers request");
     }
 }
 
@@ -148,6 +164,278 @@
     sendGetFirmwareParametersRequest(eid);
 }
 
+void InventoryManager::sendQueryDownstreamDevicesRequest(mctp_eid_t eid)
+{
+    Request requestMsg(sizeof(pldm_msg_hdr));
+    auto instanceId = instanceIdDb.next(eid);
+    auto request = new (requestMsg.data()) pldm_msg;
+    auto rc = encode_query_downstream_devices_req(instanceId, request);
+    if (rc)
+    {
+        instanceIdDb.free(eid, instanceId);
+        error(
+            "Failed to encode query downstream devices request, EID={EID}, RC = {RC}",
+            "EID", eid, "RC", rc);
+        throw std::runtime_error(
+            "Failed to encode query downstream devices request");
+    }
+
+    rc = handler.registerRequest(
+        eid, instanceId, PLDM_FWUP, PLDM_QUERY_DOWNSTREAM_DEVICES,
+        std::move(requestMsg),
+        std::bind_front(&InventoryManager::queryDownstreamDevices, this));
+    if (rc)
+    {
+        error(
+            "Failed to send QueryDownstreamDevices request, EID={EID}, RC = {RC}",
+            "EID", eid, "RC", rc);
+    }
+}
+
+void InventoryManager::queryDownstreamDevices(
+    mctp_eid_t eid, const pldm_msg* response, size_t respMsgLen)
+{
+    if (!response || !respMsgLen)
+    {
+        error("No response received for QueryDownstreamDevices, EID={EID}",
+              "EID", eid);
+        return;
+    }
+
+    pldm_query_downstream_devices_resp downstreamDevicesResp{};
+    auto rc = decode_query_downstream_devices_resp(response, respMsgLen,
+                                                   &downstreamDevicesResp);
+    if (rc)
+    {
+        error(
+            "Decoding QueryDownstreamDevices response failed, EID={EID}, RC = {RC}",
+            "EID", eid, "RC", rc);
+        return;
+    }
+
+    switch (downstreamDevicesResp.completion_code)
+    {
+        case PLDM_SUCCESS:
+            break;
+        case PLDM_ERROR_UNSUPPORTED_PLDM_CMD:
+            /* QueryDownstreamDevices is optional, consider the device does not
+             * support Downstream Devices.
+             */
+            return;
+        default:
+            error(
+                "QueryDownstreamDevices response failed with error completion code, EID={EID}, CC = {CC}",
+                "EID", eid, "CC", downstreamDevicesResp.completion_code);
+            return;
+    }
+
+    switch (downstreamDevicesResp.downstream_device_update_supported)
+    {
+        case PLDM_FWUP_DOWNSTREAM_DEVICE_UPDATE_SUPPORTED:
+            /** DataTransferHandle will be skipped when TransferOperationFlag is
+             *  `GetFirstPart`. Use 0x0 as default by following example in
+             *  Figure 9 in DSP0267 1.1.0
+             */
+            try
+            {
+                sendQueryDownstreamIdentifiersRequest(eid, 0x0,
+                                                      PLDM_GET_FIRSTPART);
+            }
+            catch (const std::exception& e)
+            {
+                error(
+                    "Failed to send QueryDownstreamIdentifiers request, EID={EID}, Error={ERROR}",
+                    "EID", eid, "ERROR", e.what());
+            }
+            break;
+        case PLDM_FWUP_DOWNSTREAM_DEVICE_UPDATE_NOT_SUPPORTED:
+            /* The FDP does not support firmware updates but may report
+             * inventory information on downstream devices.
+             * In this scenario, sends only GetDownstreamFirmwareParameters
+             * to the FDP.
+             * The definition can be found at Table 15 of DSP0267_1.1.0
+             */
+            break;
+        default:
+            error(
+                "Unknown response of DownstreamDeviceUpdateSupported from EID={EID}, Value = {VALUE}",
+                "EID", eid, "VALUE",
+                downstreamDevicesResp.downstream_device_update_supported);
+            return;
+    }
+}
+
+void InventoryManager::sendQueryDownstreamIdentifiersRequest(
+    mctp_eid_t eid, uint32_t dataTransferHandle,
+    enum transfer_op_flag transferOperationFlag)
+{
+    auto instanceId = instanceIdDb.next(eid);
+    Request requestMsg(
+        sizeof(pldm_msg_hdr) + PLDM_QUERY_DOWNSTREAM_IDENTIFIERS_REQ_BYTES);
+    auto request = new (requestMsg.data()) pldm_msg;
+    pldm_query_downstream_identifiers_req requestParameters{
+        dataTransferHandle, static_cast<uint8_t>(transferOperationFlag)};
+
+    auto rc = encode_query_downstream_identifiers_req(
+        instanceId, &requestParameters, request,
+        PLDM_QUERY_DOWNSTREAM_IDENTIFIERS_REQ_BYTES);
+    if (rc)
+    {
+        instanceIdDb.free(eid, instanceId);
+        error(
+            "Failed to encode query downstream identifiers request, EID={EID}, RC = {RC}",
+            "EID", eid, "RC", rc);
+        throw std::runtime_error(
+            "Failed to encode query downstream identifiers request");
+    }
+
+    rc = handler.registerRequest(
+        eid, instanceId, PLDM_FWUP, PLDM_QUERY_DOWNSTREAM_IDENTIFIERS,
+        std::move(requestMsg),
+        std::bind_front(&InventoryManager::queryDownstreamIdentifiers, this));
+    if (rc)
+    {
+        error(
+            "Failed to send QueryDownstreamIdentifiers request, EID={EID}, RC = {RC}",
+            "EID", eid, "RC", rc);
+    }
+}
+
+void InventoryManager::queryDownstreamIdentifiers(
+    mctp_eid_t eid, const pldm_msg* response, size_t respMsgLen)
+{
+    if (!response || !respMsgLen)
+    {
+        error("No response received for QueryDownstreamIdentifiers, EID={EID}",
+              "EID", eid);
+        descriptorMap.erase(eid);
+        return;
+    }
+
+    pldm_query_downstream_identifiers_resp downstreamIds{};
+    pldm_downstream_device_iter devs{};
+
+    auto rc = decode_query_downstream_identifiers_resp(response, respMsgLen,
+                                                       &downstreamIds, &devs);
+    if (rc)
+    {
+        error(
+            "Decoding QueryDownstreamIdentifiers response failed, EID={EID}, RC = {RC}",
+            "EID", eid, "RC", rc);
+        return;
+    }
+
+    if (downstreamIds.completion_code)
+    {
+        error(
+            "QueryDownstreamIdentifiers response failed with error completion code, EID={EID}, CC = {CC}",
+            "EID", eid, "CC", unsigned(downstreamIds.completion_code));
+        return;
+    }
+
+    DownstreamDevices initialDownstreamDevices{};
+    DownstreamDevices* downstreamDevices;
+    switch (downstreamIds.transfer_flag)
+    {
+        case PLDM_MIDDLE:
+        case PLDM_END:
+            downstreamDevices = &downstreamDescriptorMap.at(eid);
+            break;
+        default:
+            downstreamDevices = &initialDownstreamDevices;
+            break;
+    }
+
+    pldm_downstream_device dev;
+    foreach_pldm_downstream_device(devs, dev, rc)
+    {
+        pldm_descriptor desc;
+        Descriptors descriptors{};
+        foreach_pldm_downstream_device_descriptor(devs, dev, desc, rc)
+        {
+            const auto descriptorData =
+                new (const_cast<void*>(desc.descriptor_data))
+                    uint8_t[desc.descriptor_length];
+            if (desc.descriptor_type != PLDM_FWUP_VENDOR_DEFINED)
+            {
+                std::vector<uint8_t> descData(
+                    descriptorData, descriptorData + desc.descriptor_length);
+                descriptors.emplace(desc.descriptor_type, std::move(descData));
+            }
+            else
+            {
+                uint8_t descriptorTitleStrType = 0;
+                variable_field descriptorTitleStr{};
+                variable_field vendorDefinedDescriptorData{};
+
+                rc = decode_vendor_defined_descriptor_value(
+                    descriptorData, desc.descriptor_length,
+                    &descriptorTitleStrType, &descriptorTitleStr,
+                    &vendorDefinedDescriptorData);
+
+                if (rc)
+                {
+                    error(
+                        "Decoding Vendor-defined descriptor value failed, EID={EID}, RC = {RC}",
+                        "EID", eid, "RC", rc);
+                    return;
+                }
+
+                auto vendorDefinedDescriptorTitleStr =
+                    utils::toString(descriptorTitleStr);
+                std::vector<uint8_t> vendorDescData(
+                    vendorDefinedDescriptorData.ptr,
+                    vendorDefinedDescriptorData.ptr +
+                        vendorDefinedDescriptorData.length);
+                descriptors.emplace(
+                    desc.descriptor_type,
+                    std::make_tuple(vendorDefinedDescriptorTitleStr,
+                                    vendorDescData));
+            }
+        }
+        if (rc)
+        {
+            error(
+                "Failed to decode downstream descriptors from iterator, EID={EID}, RC = {RC}",
+                "EID", eid, "RC", rc);
+            return;
+        }
+        downstreamDevices->emplace_back(
+            DownstreamDeviceInfo{dev.downstream_device_index, descriptors});
+    }
+    if (rc)
+    {
+        error(
+            "Failed to decode downstream devices from iterator, EID={EID}, RC = {RC}",
+            "EID", eid, "RC", rc);
+        return;
+    }
+
+    switch (downstreamIds.transfer_flag)
+    {
+        case PLDM_START:
+            downstreamDescriptorMap.emplace(
+                eid, std::move(initialDownstreamDevices));
+            [[fallthrough]];
+        case PLDM_MIDDLE:
+            sendQueryDownstreamIdentifiersRequest(
+                eid, downstreamIds.next_data_transfer_handle,
+                PLDM_GET_NEXTPART);
+            break;
+        case PLDM_START_AND_END:
+            downstreamDescriptorMap.emplace(
+                eid, std::move(initialDownstreamDevices));
+            /** DataTransferHandle will be skipped when TransferOperationFlag is
+             *  `GetFirstPart`. Use 0x0 as default by following example in
+             *  Figure 9 in DSP0267 1.1.0
+             */
+            [[fallthrough]];
+        case PLDM_END:
+            sendQueryDeviceIdentifiersRequest(eid);
+            break;
+    }
+}
+
 void InventoryManager::sendGetFirmwareParametersRequest(mctp_eid_t eid)
 {
     auto instanceId = instanceIdDb.next(eid);
diff --git a/fw-update/inventory_manager.hpp b/fw-update/inventory_manager.hpp
index 953d7a4..69ce1c9 100644
--- a/fw-update/inventory_manager.hpp
+++ b/fw-update/inventory_manager.hpp
@@ -34,15 +34,21 @@
      *  @param[in] instanceIdDb - Managing instance ID for PLDM requests
      *  @param[out] descriptorMap - Populate the firmware identifiers for the
      *                              FDs managed by the BMC.
+     *  @param[out] downstreamDescriptorMap - Populate the downstream
+     *                                        identifiers for the FDs managed
+     *                                        by the BMC.
      *  @param[out] componentInfoMap - Populate the component info for the FDs
      *                                 managed by the BMC.
      */
     explicit InventoryManager(
         pldm::requester::Handler<pldm::requester::Request>& handler,
         InstanceIdDb& instanceIdDb, DescriptorMap& descriptorMap,
+        DownstreamDescriptorMap& downstreamDescriptorMap,
         ComponentInfoMap& componentInfoMap) :
         handler(handler), instanceIdDb(instanceIdDb),
-        descriptorMap(descriptorMap), componentInfoMap(componentInfoMap)
+        descriptorMap(descriptorMap),
+        downstreamDescriptorMap(downstreamDescriptorMap),
+        componentInfoMap(componentInfoMap)
     {}
 
     /** @brief Discover the firmware identifiers and component details of FDs
@@ -68,6 +74,24 @@
     void queryDeviceIdentifiers(mctp_eid_t eid, const pldm_msg* response,
                                 size_t respMsgLen);
 
+    /** @brief Handler for QueryDownstreamDevices command response
+     *
+     *  @param[in] eid - Remote MCTP endpoint
+     *  @param[in] response - PLDM response message
+     *  @param[in] respMsgLen - Response message length
+     */
+    void queryDownstreamDevices(mctp_eid_t eid, const pldm_msg* response,
+                                size_t respMsgLen);
+
+    /** @brief Handler for QueryDownstreamIdentifiers command response
+     *
+     *  @param[in] eid - Remote MCTP endpoint
+     *  @param[in] response - PLDM response message
+     *  @param[in] respMsgLen - Response message length
+     */
+    void queryDownstreamIdentifiers(mctp_eid_t eid, const pldm_msg* response,
+                                    size_t respMsgLen);
+
     /** @brief Handler for GetFirmwareParameters command response
      *
      *  Handling the response of GetFirmwareParameters command and create
@@ -81,6 +105,34 @@
                                size_t respMsgLen);
 
   private:
+    /**
+     * @brief Sends QueryDeviceIdentifiers request
+     *
+     * @param[in] eid - Remote MCTP endpoint
+     */
+    void sendQueryDeviceIdentifiersRequest(mctp_eid_t eid);
+
+    /**
+     * @brief Sends QueryDownstreamDevices request
+     *
+     * @param[in] eid - Remote MCTP endpoint
+     */
+    void sendQueryDownstreamDevicesRequest(mctp_eid_t eid);
+
+    /**
+     * @brief Sends QueryDownstreamIdentifiers request
+     *
+     * The request format is defined at Table 16 – QueryDownstreamIdentifiers
+     * command format in DSP0267_1.1.0
+     *
+     * @param[in] eid - Remote MCTP endpoint
+     * @param[in] dataTransferHandle - Data transfer handle
+     * @param[in] transferOperationFlag - Transfer operation flag
+     */
+    void sendQueryDownstreamIdentifiersRequest(
+        mctp_eid_t eid, uint32_t dataTransferHandle,
+        enum transfer_op_flag transferOperationFlag);
+
     /** @brief Send GetFirmwareParameters command request
      *
      *  @param[in] eid - Remote MCTP endpoint
@@ -96,6 +148,9 @@
     /** @brief Device identifiers of the managed FDs */
     DescriptorMap& descriptorMap;
 
+    /** @brief Downstream Device identifiers of the managed FDs */
+    DownstreamDescriptorMap& downstreamDescriptorMap;
+
     /** @brief Component information needed for the update of the managed FDs */
     ComponentInfoMap& componentInfoMap;
 };
diff --git a/fw-update/manager.hpp b/fw-update/manager.hpp
index be45eb7..4d2a976 100644
--- a/fw-update/manager.hpp
+++ b/fw-update/manager.hpp
@@ -40,7 +40,8 @@
     explicit Manager(Event& event,
                      requester::Handler<requester::Request>& handler,
                      pldm::InstanceIdDb& instanceIdDb) :
-        inventoryMgr(handler, instanceIdDb, descriptorMap, componentInfoMap),
+        inventoryMgr(handler, instanceIdDb, descriptorMap,
+                     downstreamDescriptorMap, componentInfoMap),
         updateManager(event, handler, instanceIdDb, descriptorMap,
                       componentInfoMap)
     {}
@@ -90,6 +91,10 @@
     /** Descriptor information of all the discovered MCTP endpoints */
     DescriptorMap descriptorMap;
 
+    /** Downstream descriptor information of all the discovered MCTP endpoints
+     */
+    DownstreamDescriptorMap downstreamDescriptorMap;
+
     /** Component information of all the discovered MCTP endpoints */
     ComponentInfoMap componentInfoMap;
 
diff --git a/fw-update/test/inventory_manager_test.cpp b/fw-update/test/inventory_manager_test.cpp
index 9d80be2..678d9bb 100644
--- a/fw-update/test/inventory_manager_test.cpp
+++ b/fw-update/test/inventory_manager_test.cpp
@@ -19,7 +19,7 @@
         reqHandler(nullptr, event, instanceIdDb, false, seconds(1), 2,
                    milliseconds(100)),
         inventoryManager(reqHandler, instanceIdDb, outDescriptorMap,
-                         outComponentInfoMap)
+                         outDownstreamDescriptorMap, outComponentInfoMap)
     {}
 
     int fd = -1;
@@ -28,6 +28,7 @@
     requester::Handler<requester::Request> reqHandler;
     InventoryManager inventoryManager;
     DescriptorMap outDescriptorMap{};
+    DownstreamDescriptorMap outDownstreamDescriptorMap{};
     ComponentInfoMap outComponentInfoMap{};
 };
 
@@ -100,6 +101,51 @@
     EXPECT_EQ(outDescriptorMap.size(), 0);
 }
 
+TEST_F(InventoryManagerTest, handleQueryDownstreamIdentifierResponse)
+{
+    constexpr uint8_t eid = 1;
+    constexpr uint8_t downstreamDeviceCount = 1;
+    constexpr uint32_t downstreamDeviceLen = 11;
+    constexpr size_t respPayloadLength =
+        PLDM_QUERY_DOWNSTREAM_IDENTIFIERS_RESP_MIN_LEN + downstreamDeviceLen;
+
+    std::array<uint8_t, sizeof(pldm_msg_hdr) + respPayloadLength>
+        queryDownstreamIdentifiersResp{
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
+            0x0B, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
+            0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0xa0, 0x15};
+    auto responseMsg = new (queryDownstreamIdentifiersResp.data()) pldm_msg;
+
+    inventoryManager.queryDownstreamIdentifiers(eid, responseMsg,
+                                                respPayloadLength);
+
+    DownstreamDevices downstreamDevices = {
+        {0,
+         {{PLDM_FWUP_IANA_ENTERPRISE_ID,
+           std::vector<uint8_t>{0x00, 0x00, 0xa0, 0x15}}}}};
+    DownstreamDescriptorMap refDownstreamDescriptorMap{
+        {eid, downstreamDevices}};
+
+    ASSERT_EQ(outDownstreamDescriptorMap.size(), downstreamDeviceCount);
+    ASSERT_EQ(outDownstreamDescriptorMap.size(),
+              refDownstreamDescriptorMap.size());
+    ASSERT_EQ(outDownstreamDescriptorMap, refDownstreamDescriptorMap);
+}
+
+TEST_F(InventoryManagerTest, handleQueryDownstreamIdentifierResponseErrorCC)
+{
+    constexpr size_t respPayloadLength = 1;
+    constexpr std::array<uint8_t, sizeof(pldm_msg_hdr) + respPayloadLength>
+        queryDownstreamIdentifiersResp{0x00, 0x00, 0x00, 0x01};
+    const auto responseMsg =
+        new (const_cast<unsigned char*>(queryDownstreamIdentifiersResp.data()))
+            pldm_msg;
+    inventoryManager.queryDownstreamIdentifiers(1, responseMsg,
+                                                respPayloadLength);
+
+    ASSERT_EQ(outDownstreamDescriptorMap.size(), 0);
+}
+
 TEST_F(InventoryManagerTest, getFirmwareParametersResponse)
 {
     // constexpr uint16_t compCount = 2;