Implement LocationIndicatorActive for Fan

Implement LocationIndicatorActive for Fan schema to set and get the
status of the location LED for each Fan.[1] A client uses the
`LocationIndicatorActive` property to physically identify or locate the
processor.

Uses the utility functions getLocationIndicatorActive() and
setLocationIndicatorActive() to follow the association and get or set
the LED value.

[1] https://redfish.dmtf.org/schemas/v1/Fan.v1_5_2.json

Tested:
- Validator passes
- Tested on p10bmc hardware simulator:

1. Get LocationIndicatorActive
```
curl -k -H "X-Auth-Token: $token" -X GET https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1
{
  "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1",
  "@odata.type": "#Fan.v1_3_0.Fan",
  "Id": "fan1",
  ...
  "LocationIndicatorActive": false,
  ...
}
```

2. Set LocationIndicatorActive to true
```
curl -k -H "X-Auth-Token: $token" -H "Content-Type: application/json" -X PATCH -d '{"LocationIndicatorActive":true}' https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1

curl -k -H "X-Auth-Token: $token" -X GET https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1
{
  "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1",
  "@odata.type": "#Fan.v1_3_0.Fan",
  "Id": "fan1",
  ...
  "LocationIndicatorActive": true,
  ...
}
```

3. Set LocationIndicatorActive to false
```
curl -k -H "X-Auth-Token: $token" -H "Content-Type: application/json" -X PATCH -d '{"LocationIndicatorActive":false}' https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1

curl -k -H "X-Auth-Token: $token" -X GET https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1
{
  "@odata.id": "/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1",
  "@odata.type": "#Fan.v1_3_0.Fan",
  "Id": "fan1",
  ...
  "LocationIndicatorActive": false,
  ...
}
```

4. Error returned when trying to set the value to non-boolean
```
 curl -k -H "X-Auth-Token: $token" -H "Content-Type: application/json" -X PATCH -d '{"LocationIndicatorActive":"badValue"}' https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan1
{
  "LocationIndicatorActive@Message.ExtendedInfo": [
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The value '\"badValue\"' for the property LocationIndicatorActive is not a type that the property can accept.",
      "MessageArgs": [
        "\"badValue\"",
        "LocationIndicatorActive"
      ],
      "MessageId": "Base.1.19.PropertyValueTypeError",
      "MessageSeverity": "Warning",
      "Resolution": "Correct the value for the property in the request body and resubmit the request if the operation failed."
    }
  ]
}
```

5. Error returned when specifying an unknown Fan resource
```
curl -k -H "X-Auth-Token: $token" -X GET https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan15
{
  "error": {
    "@Message.ExtendedInfo": [
      {
        "@odata.type": "#Message.v1_1_1.Message",
        "Message": "The requested resource of type Fan named 'fan15' was not found.",
        "MessageArgs": [
          "Fan",
          "fan15"
        ],
        "MessageId": "Base.1.19.ResourceNotFound",
        "MessageSeverity": "Critical",
        "Resolution": "Provide a valid resource identifier and resubmit the request."
      }
    ],
    "code": "Base.1.19.ResourceNotFound",
    "message": "The requested resource of type Fan named 'fan15' was not found."
  }
}

curl -k -H "X-Auth-Token: $token" -H "Content-Type: application/json" -X PATCH -d '{"LocationIndicatorActive":true}' https://${bmc}/redfish/v1/Chassis/chassis/ThermalSubsystem/Fans/fan15
{
  "error": {
    "@Message.ExtendedInfo": [
      {
        "@odata.type": "#Message.v1_1_1.Message",
        "Message": "The requested resource of type Fan named 'fan15' was not found.",
        "MessageArgs": [
          "Fan",
          "fan15"
        ],
        "MessageId": "Base.1.19.ResourceNotFound",
        "MessageSeverity": "Critical",
        "Resolution": "Provide a valid resource identifier and resubmit the request."
      }
    ],
    "code": "Base.1.19.ResourceNotFound",
    "message": "The requested resource of type Fan named 'fan15' was not found."
  }
}
```

Signed-off-by: Albert Zhang <zhanghaodi@inspur.com>
Signed-off-by: Lakshmi Yadlapati <lakshmiy@us.ibm.com>
Signed-off-by: Myung Bae <myungbae@us.ibm.com>
Signed-off-by: Janet Adkins <janeta@us.ibm.com>
Change-Id: Ibc0da1714120b03c9a06241be1f721561b4d0d8b
diff --git a/Redfish.md b/Redfish.md
index cc6999c..3770aab 100644
--- a/Redfish.md
+++ b/Redfish.md
@@ -376,6 +376,7 @@
 #### Fan
 
 - Location
+- LocationIndicatorActive
 - Manufacturer
 - Model
 - PartNumber
diff --git a/redfish-core/lib/fan.hpp b/redfish-core/lib/fan.hpp
index baa25b3..c10288d 100644
--- a/redfish-core/lib/fan.hpp
+++ b/redfish-core/lib/fan.hpp
@@ -8,11 +8,13 @@
 #include "error_messages.hpp"
 #include "generated/enums/resource.hpp"
 #include "http_request.hpp"
+#include "led.hpp"
 #include "logging.hpp"
 #include "query.hpp"
 #include "registries/privilege_registry.hpp"
 #include "utils/chassis_utils.hpp"
 #include "utils/dbus_utils.hpp"
+#include "utils/json_utils.hpp"
 
 #include <asm-generic/errno.h>
 
@@ -213,7 +215,7 @@
     messages::resourceNotFound(asyncResp->res, "Fan", fanId);
 }
 
-inline void getValidFanPath(
+inline void getValidFanObject(
     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
     const std::string& validChassisPath, const std::string& fanId,
     const std::function<void(const std::string& fanPath,
@@ -375,7 +377,7 @@
         });
 }
 
-inline void afterGetValidFanPath(
+inline void afterGetValidFanObject(
     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
     const std::string& chassisId, const std::string& fanId,
     const std::string& fanPath, const std::string& service)
@@ -385,6 +387,7 @@
     getFanHealth(asyncResp, fanPath, service);
     getFanAsset(asyncResp, fanPath, service);
     getFanLocation(asyncResp, fanPath, service);
+    getLocationIndicatorActive(asyncResp, fanPath);
 }
 
 inline void doFanGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
@@ -397,9 +400,9 @@
         return;
     }
 
-    getValidFanPath(
+    getValidFanObject(
         asyncResp, *validChassisPath, fanId,
-        std::bind_front(afterGetValidFanPath, asyncResp, chassisId, fanId));
+        std::bind_front(afterGetValidFanObject, asyncResp, chassisId, fanId));
 }
 
 inline void handleFanHead(App& app, const crow::Request& req,
@@ -422,7 +425,7 @@
                                            chassisId);
                 return;
             }
-            getValidFanPath(
+            getValidFanObject(
                 asyncResp, *validChassisPath, fanId,
                 [asyncResp](const std::string&, const std::string&) {
                     asyncResp->res.addHeader(
@@ -446,6 +449,73 @@
         std::bind_front(doFanGet, asyncResp, chassisId, fanId));
 }
 
+inline void handleSetFanPathById(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const std::string& chassisId, const std::string& fanId,
+    bool locationIndicatorActive, const boost::system::error_code& ec,
+    const dbus::utility::MapperGetSubTreePathsResponse& fanPaths)
+{
+    if (ec)
+    {
+        if (ec.value() == boost::system::errc::io_error)
+        {
+            BMCWEB_LOG_WARNING("Chassis {} not found", chassisId);
+            messages::resourceNotFound(asyncResp->res, "Chassis", chassisId);
+            return;
+        }
+
+        BMCWEB_LOG_ERROR("DBUS response error {}", ec.value());
+        messages::internalError(asyncResp->res);
+        return;
+    }
+
+    for (const auto& fanPath : fanPaths)
+    {
+        if (checkFanId(fanPath, fanId))
+        {
+            setLocationIndicatorActive(asyncResp, fanPath,
+                                       locationIndicatorActive);
+            return;
+        }
+    }
+    BMCWEB_LOG_WARNING("Fan {} not found", fanId);
+    messages::resourceNotFound(asyncResp->res, "Fan", fanId);
+}
+
+inline void handleFanPatch(App& app, const crow::Request& req,
+                           const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                           const std::string& chassisId,
+                           const std::string& fanId)
+{
+    if (!redfish::setUpRedfishRoute(app, req, asyncResp))
+    {
+        return;
+    }
+
+    std::optional<bool> locationIndicatorActive;
+    if (!json_util::readJsonPatch(req, asyncResp->res,
+                                  "LocationIndicatorActive",
+                                  locationIndicatorActive))
+    {
+        return;
+    }
+
+    if (locationIndicatorActive)
+    {
+        dbus::utility::getAssociatedSubTreePathsById(
+            chassisId, "/xyz/openbmc_project/inventory", chassisInterfaces,
+            "cooled_by", fanInterface,
+            [asyncResp, chassisId, fanId, locationIndicatorActive](
+                const boost::system::error_code& ec,
+                const dbus::utility::MapperGetSubTreePathsResponse&
+                    subtreePaths) {
+                handleSetFanPathById(asyncResp, chassisId, fanId,
+                                     *locationIndicatorActive, ec,
+                                     subtreePaths);
+            });
+    }
+}
+
 inline void requestRoutesFan(App& app)
 {
     BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/<str>/")
@@ -457,6 +527,11 @@
         .privileges(redfish::privileges::getFan)
         .methods(boost::beast::http::verb::get)(
             std::bind_front(handleFanGet, std::ref(app)));
+
+    BMCWEB_ROUTE(app, "/redfish/v1/Chassis/<str>/ThermalSubsystem/Fans/<str>/")
+        .privileges(redfish::privileges::patchFan)
+        .methods(boost::beast::http::verb::patch)(
+            std::bind_front(handleFanPatch, std::ref(app)));
 }
 
 } // namespace redfish