Handle Processor.AppliedOperatingConfig PATCH

Allow client to PATCH the URI pointing to the active OperatingConfig for
a given processor, in order to dynamically change the processor's
configuration. The OperatingConfig related properties are only available
if a supporting backend daemon is running - currently we have the Intel
cpuinfoapp in the smbios-mdr repo.

This change does basic validation on input data, then sets the D-Bus
property and translates any return errors into Redfish error messages.

Tested:
- PATCH non-existent Processor -> ResourceNotFound
- PATCH with string/int -> PropertyValueTypeError
- PATCH with object w/o @odata.id -> PropertyMissing+Unknown
- PATCH with config from different Processor -> PropertyValueIncorrect
- PATCH with non-existent config -> PropertyValueIncorrect
- PATCH with valid config -> OK, see new config on next GET

- Hack cpuinfoapp backend service to return all possible error codes and
  verify that correct Redfish error is returned.

Change-Id: I19d8b461fac25682f1883fe769d840f18f4141a8
Signed-off-by: Jonathan Doman <jonathan.doman@intel.com>
diff --git a/redfish-core/lib/processor.hpp b/redfish-core/lib/processor.hpp
index bd18707..49e04e2 100644
--- a/redfish-core/lib/processor.hpp
+++ b/redfish-core/lib/processor.hpp
@@ -847,6 +847,140 @@
         "xyz.openbmc_project.Inventory.Item.Cpu.OperatingConfig");
 }
 
+/**
+ * Handle the D-Bus response from attempting to set the CPU's AppliedConfig
+ * property. Main task is to translate error messages into Redfish errors.
+ *
+ * @param[in,out]   resp    HTTP response.
+ * @param[in]       setPropVal  Value which we attempted to set.
+ * @param[in]       ec      D-Bus response error code.
+ * @param[in]       msg     D-Bus response message.
+ */
+inline void
+    handleAppliedConfigResponse(const std::shared_ptr<bmcweb::AsyncResp>& resp,
+                                const std::string& setPropVal,
+                                boost::system::error_code ec,
+                                const sdbusplus::message::message& msg)
+{
+    if (!ec)
+    {
+        BMCWEB_LOG_DEBUG << "Set Property succeeded";
+        return;
+    }
+
+    BMCWEB_LOG_DEBUG << "Set Property failed: " << ec;
+
+    const sd_bus_error* dbusError = msg.get_error();
+    if (dbusError == nullptr)
+    {
+        messages::internalError(resp->res);
+        return;
+    }
+
+    // The asio error code doesn't know about our custom errors, so we have to
+    // parse the error string. Some of these D-Bus -> Redfish translations are a
+    // stretch, but it's good to try to communicate something vaguely useful.
+    if (strcmp(dbusError->name,
+               "xyz.openbmc_project.Common.Error.InvalidArgument") == 0)
+    {
+        // Service did not like the object_path we tried to set.
+        messages::propertyValueIncorrect(
+            resp->res, "AppliedOperatingConfig/@odata.id", setPropVal);
+    }
+    else if (strcmp(dbusError->name,
+                    "xyz.openbmc_project.Common.Error.NotAllowed") == 0)
+    {
+        // Service indicates we can never change the config for this processor.
+        messages::propertyNotWritable(resp->res, "AppliedOperatingConfig");
+    }
+    else if (strcmp(dbusError->name,
+                    "xyz.openbmc_project.Common.Error.Unavailable") == 0)
+    {
+        // Service indicates the config cannot be changed right now, but maybe
+        // in a different system state.
+        messages::resourceInStandby(resp->res);
+    }
+    else if (strcmp(dbusError->name,
+                    "xyz.openbmc_project.Common.Device.Error.WriteFailure") ==
+             0)
+    {
+        // Service tried to change the config, but it failed.
+        messages::operationFailed(resp->res);
+    }
+    else
+    {
+        messages::internalError(resp->res);
+    }
+}
+
+/**
+ * Handle the PATCH operation of the AppliedOperatingConfig property. Do basic
+ * validation of the input data, and then set the D-Bus property.
+ *
+ * @param[in,out]   resp            Async HTTP response.
+ * @param[in]       processorId     Processor's Id.
+ * @param[in]       appliedConfigUri    New property value to apply.
+ * @param[in]       cpuObjectPath   Path of CPU object to modify.
+ * @param[in]       serviceMap      Service map for CPU object.
+ */
+inline void patchAppliedOperatingConfig(
+    const std::shared_ptr<bmcweb::AsyncResp>& resp,
+    const std::string& processorId, const std::string& appliedConfigUri,
+    const std::string& cpuObjectPath, const MapperServiceMap& serviceMap)
+{
+    // Check that the property even exists by checking for the interface
+    const std::string* controlService = nullptr;
+    for (const auto& [serviceName, interfaceList] : serviceMap)
+    {
+        if (std::find(interfaceList.begin(), interfaceList.end(),
+                      "xyz.openbmc_project.Control.Processor."
+                      "CurrentOperatingConfig") != interfaceList.end())
+        {
+            controlService = &serviceName;
+            break;
+        }
+    }
+
+    if (controlService == nullptr)
+    {
+        messages::internalError(resp->res);
+        return;
+    }
+
+    // Check that the config URI is a child of the cpu URI being patched.
+    std::string expectedPrefix("/redfish/v1/Systems/system/Processors/");
+    expectedPrefix += processorId;
+    expectedPrefix += "/OperatingConfigs/";
+    if (!boost::starts_with(appliedConfigUri, expectedPrefix) ||
+        expectedPrefix.size() == appliedConfigUri.size())
+    {
+        messages::propertyValueIncorrect(
+            resp->res, "AppliedOperatingConfig/@odata.id", appliedConfigUri);
+        return;
+    }
+
+    // Generate the D-Bus path of the OperatingConfig object, by assuming it's a
+    // direct child of the CPU object.
+    // Strip the expectedPrefix from the config URI to get the "filename", and
+    // append to the CPU's path.
+    std::string configBaseName = appliedConfigUri.substr(expectedPrefix.size());
+    sdbusplus::message::object_path configPath(cpuObjectPath);
+    configPath /= configBaseName;
+
+    BMCWEB_LOG_INFO << "Setting config to " << configPath.str;
+
+    // Set the property, with handler to check error responses
+    crow::connections::systemBus->async_method_call(
+        [resp, appliedConfigUri](boost::system::error_code ec,
+                                 sdbusplus::message::message& msg) {
+            handleAppliedConfigResponse(resp, appliedConfigUri, ec, msg);
+        },
+        *controlService, cpuObjectPath, "org.freedesktop.DBus.Properties",
+        "Set", "xyz.openbmc_project.Control.Processor.CurrentOperatingConfig",
+        "AppliedConfig",
+        std::variant<sdbusplus::message::object_path>(std::move(configPath)));
+}
+
 class OperatingConfigCollection : public Node
 {
   public:
@@ -1087,6 +1221,41 @@
 
         getProcessorObject(asyncResp, processorId, getProcessorData);
     }
+
+    void doPatch(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                 const crow::Request& req,
+                 const std::vector<std::string>& params) override
+    {
+        std::optional<nlohmann::json> appliedConfigJson;
+        if (!json_util::readJson(req, asyncResp->res, "AppliedOperatingConfig",
+                                 appliedConfigJson))
+        {
+            return;
+        }
+
+        std::string appliedConfigUri;
+        if (appliedConfigJson)
+        {
+            if (!json_util::readJson(*appliedConfigJson, asyncResp->res,
+                                     "@odata.id", appliedConfigUri))
+            {
+                return;
+            }
+            // Check for 404 and find matching D-Bus object, then run property
+            // patch handlers if that all succeeds.
+            getProcessorObject(
+                asyncResp, params[0],
+                [appliedConfigUri = std::move(appliedConfigUri)](
+                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+                    const std::string& processorId,
+                    const std::string& objectPath,
+                    const MapperServiceMap& serviceMap) {
+                    patchAppliedOperatingConfig(asyncResp, processorId,
+                                                appliedConfigUri, objectPath,
+                                                serviceMap);
+                });
+        }
+    }
 };
 
 } // namespace redfish