Add PID Patch

This allows runtime modifying Pid configurations
and zones.

Tested by:
Sent PATCH for:
Modify FanController
Modify FanZone
New FanController
New FanZone
Modify PidController
New PidController
Delete FanController
Delete FanZone

Change-Id: Ie2dd52ea0b09dc4ad1ed016ec4c27b3c39be28de
Signed-off-by: James Feist <james.feist@linux.intel.com>
diff --git a/redfish-core/lib/managers.hpp b/redfish-core/lib/managers.hpp
index 9e9d150..12ee3b6 100644
--- a/redfish-core/lib/managers.hpp
+++ b/redfish-core/lib/managers.hpp
@@ -272,6 +272,294 @@
         connection, path, objectManagerIface, "GetManagedObjects");
 }
 
+enum class CreatePIDRet
+{
+    fail,
+    del,
+    patch
+};
+
+static CreatePIDRet createPidInterface(
+    const std::shared_ptr<AsyncResp>& response, const std::string& type,
+    const nlohmann::json& record, const std::string& path,
+    const dbus::utility::ManagedObjectType& managedObj, bool createNewObject,
+    boost::container::flat_map<std::string, dbus::utility::DbusVariantType>&
+        output,
+    std::string& chassis)
+{
+
+    if (type == "PidControllers" || type == "FanControllers")
+    {
+        if (createNewObject)
+        {
+            output["Class"] = type == "PidControllers" ? std::string("temp")
+                                                       : std::string("fan");
+            output["Type"] = std::string("Pid");
+        }
+        else if (record == nullptr)
+        {
+            // delete interface
+            crow::connections::systemBus->async_method_call(
+                [response,
+                 path{std::string(path)}](const boost::system::error_code ec) {
+                    if (ec)
+                    {
+                        BMCWEB_LOG_ERROR << "Error patching " << path << ": "
+                                         << ec;
+                        response->res.result(
+                            boost::beast::http::status::internal_server_error);
+                    }
+                },
+                "xyz.openbmc_project.EntityManager", path,
+                pidConfigurationIface, "Delete");
+            return CreatePIDRet::del;
+        }
+
+        for (auto& field : record.items())
+        {
+            if (field.key() == "Zones")
+            {
+                if (!field.value().is_array())
+                {
+                    BMCWEB_LOG_ERROR << "Illegal Type " << field.key();
+                    messages::addMessageToErrorJson(
+                        response->res.jsonValue,
+                        messages::propertyValueFormatError(field.value(),
+                                                           field.key()));
+                    response->res.result(
+                        boost::beast::http::status::bad_request);
+                    return CreatePIDRet::fail;
+                }
+                std::vector<std::string> inputs;
+                for (const auto& odata : field.value().items())
+                {
+                    for (const auto& value : odata.value().items())
+                    {
+                        const std::string* path =
+                            value.value().get_ptr<const std::string*>();
+                        if (path == nullptr)
+                        {
+                            BMCWEB_LOG_ERROR << "Illegal Type " << field.key();
+                            messages::addMessageToErrorJson(
+                                response->res.jsonValue,
+                                messages::propertyValueFormatError(
+                                    field.value().dump(), field.key()));
+                            response->res.result(
+                                boost::beast::http::status::bad_request);
+                            return CreatePIDRet::fail;
+                        }
+                        std::string input;
+                        if (!dbus::utility::getNthStringFromPath(*path, 4,
+                                                                 input))
+                        {
+                            BMCWEB_LOG_ERROR << "Got invalid path " << *path;
+                            messages::addMessageToErrorJson(
+                                response->res.jsonValue,
+                                messages::propertyValueFormatError(
+                                    field.value().dump(), field.key()));
+                            response->res.result(
+                                boost::beast::http::status::bad_request);
+                            return CreatePIDRet::fail;
+                        }
+                        boost::replace_all(input, "_", " ");
+                        inputs.emplace_back(std::move(input));
+                    }
+                }
+                output["Zones"] = std::move(inputs);
+            }
+            else if (field.key() == "Inputs" || field.key() == "Outputs")
+            {
+                if (!field.value().is_array())
+                {
+                    BMCWEB_LOG_ERROR << "Illegal Type " << field.key();
+                    messages::addMessageToErrorJson(
+                        response->res.jsonValue,
+                        messages::propertyValueFormatError(field.value().dump(),
+                                                           field.key()));
+                    response->res.result(
+                        boost::beast::http::status::bad_request);
+                    return CreatePIDRet::fail;
+                }
+                std::vector<std::string> inputs;
+                for (const auto& value : field.value().items())
+                {
+                    const std::string* sensor =
+                        value.value().get_ptr<const std::string*>();
+
+                    if (sensor == nullptr)
+                    {
+                        BMCWEB_LOG_ERROR << "Illegal Type "
+                                         << field.value().dump();
+                        messages::addMessageToErrorJson(
+                            response->res.jsonValue,
+                            messages::propertyValueFormatError(
+                                field.value().dump(), field.key()));
+                        response->res.result(
+                            boost::beast::http::status::bad_request);
+                        return CreatePIDRet::fail;
+                    }
+
+                    std::string input =
+                        boost::replace_all_copy(*sensor, "_", " ");
+                    inputs.push_back(std::move(input));
+                    // try to find the sensor in the
+                    // configuration
+                    if (chassis.empty())
+                    {
+                        std::find_if(
+                            managedObj.begin(), managedObj.end(),
+                            [&chassis, sensor](const auto& obj) {
+                                if (boost::algorithm::ends_with(obj.first.str,
+                                                                *sensor))
+                                {
+                                    return dbus::utility::getNthStringFromPath(
+                                        obj.first.str, 5, chassis);
+                                }
+                                return false;
+                            });
+                    }
+                }
+                output[field.key()] = inputs;
+            }
+
+            // doubles
+            else if (field.key() == "FFGainCoefficient" ||
+                     field.key() == "FFOffCoefficient" ||
+                     field.key() == "ICoefficient" ||
+                     field.key() == "ILimitMax" || field.key() == "ILimitMin" ||
+                     field.key() == "OutLimitMax" ||
+                     field.key() == "OutLimitMin" ||
+                     field.key() == "PCoefficient" ||
+                     field.key() == "SetPoint" || field.key() == "SlewNeg" ||
+                     field.key() == "SlewPos")
+            {
+                const double* ptr = field.value().get_ptr<const double*>();
+                if (ptr == nullptr)
+                {
+                    BMCWEB_LOG_ERROR << "Illegal Type " << field.key();
+                    messages::addMessageToErrorJson(
+                        response->res.jsonValue,
+                        messages::propertyValueFormatError(field.value().dump(),
+                                                           field.key()));
+                    response->res.result(
+                        boost::beast::http::status::bad_request);
+                    return CreatePIDRet::fail;
+                }
+                output[field.key()] = *ptr;
+            }
+
+            else
+            {
+                BMCWEB_LOG_ERROR << "Illegal Type " << field.key();
+                messages::addMessageToErrorJson(
+                    response->res.jsonValue,
+                    messages::propertyUnknown(field.key()));
+                response->res.result(boost::beast::http::status::bad_request);
+                return CreatePIDRet::fail;
+            }
+        }
+    }
+    else if (type == "FanZones")
+    {
+        if (!createNewObject && record == nullptr)
+        {
+            // delete interface
+            crow::connections::systemBus->async_method_call(
+                [response,
+                 path{std::string(path)}](const boost::system::error_code ec) {
+                    if (ec)
+                    {
+                        BMCWEB_LOG_ERROR << "Error patching " << path << ": "
+                                         << ec;
+                        response->res.result(
+                            boost::beast::http::status::internal_server_error);
+                    }
+                },
+                "xyz.openbmc_project.EntityManager", path,
+                pidZoneConfigurationIface, "Delete");
+            return CreatePIDRet::del;
+        }
+        output["Type"] = std::string("Pid.Zone");
+
+        for (auto& field : record.items())
+        {
+            if (field.key() == "Chassis")
+            {
+                const std::string* chassisId = nullptr;
+                for (const auto& id : field.value().items())
+                {
+                    if (id.key() != "@odata.id")
+                    {
+                        BMCWEB_LOG_ERROR << "Illegal Type " << id.key();
+                        messages::addMessageToErrorJson(
+                            response->res.jsonValue,
+                            messages::propertyUnknown(field.key()));
+                        response->res.result(
+                            boost::beast::http::status::bad_request);
+                        return CreatePIDRet::fail;
+                    }
+                    chassisId = id.value().get_ptr<const std::string*>();
+                    if (chassisId == nullptr)
+                    {
+                        messages::addMessageToErrorJson(
+                            response->res.jsonValue,
+                            messages::createFailedMissingReqProperties(
+                                field.key()));
+                        response->res.result(
+                            boost::beast::http::status::bad_request);
+                        return CreatePIDRet::fail;
+                    }
+                }
+
+                // /refish/v1/chassis/chassis_name/
+                if (!dbus::utility::getNthStringFromPath(*chassisId, 3,
+                                                         chassis))
+                {
+                    BMCWEB_LOG_ERROR << "Got invalid path " << *chassisId;
+                    response->res.result(
+                        boost::beast::http::status::bad_request);
+                    return CreatePIDRet::fail;
+                }
+            }
+            else if (field.key() == "FailSafePercent" ||
+                     field.key() == "MinThermalRpm")
+            {
+                const double* ptr = field.value().get_ptr<const double*>();
+                if (ptr == nullptr)
+                {
+                    BMCWEB_LOG_ERROR << "Illegal Type " << field.key();
+                    messages::addMessageToErrorJson(
+                        response->res.jsonValue,
+                        messages::propertyValueFormatError(field.value().dump(),
+                                                           field.key()));
+                    response->res.result(
+                        boost::beast::http::status::bad_request);
+                    return CreatePIDRet::fail;
+                }
+                output[field.key()] = *ptr;
+            }
+            else
+            {
+                BMCWEB_LOG_ERROR << "Illegal Type " << field.key();
+                messages::addMessageToErrorJson(
+                    response->res.jsonValue,
+                    messages::propertyUnknown(field.key()));
+                response->res.result(boost::beast::http::status::bad_request);
+                return CreatePIDRet::fail;
+            }
+        }
+    }
+    else
+    {
+        BMCWEB_LOG_ERROR << "Illegal Type " << type;
+        messages::addMessageToErrorJson(response->res.jsonValue,
+                                        messages::propertyUnknown(type));
+        response->res.result(boost::beast::http::status::bad_request);
+        return CreatePIDRet::fail;
+    }
+    return CreatePIDRet::patch;
+}
+
 class Manager : public Node
 {
   public:
@@ -433,10 +721,256 @@
             "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
         getPidValues(asyncResp);
     }
+    void setPidValues(std::shared_ptr<AsyncResp> response,
+                      const nlohmann::json& data)
+    {
+        // todo(james): might make sense to do a mapper call here if this
+        // interface gets more traction
+        crow::connections::systemBus->async_method_call(
+            [response,
+             data](const boost::system::error_code ec,
+                   const dbus::utility::ManagedObjectType& managedObj) {
+                if (ec)
+                {
+                    BMCWEB_LOG_ERROR << "Error communicating to Entity Manager";
+                    response->res.result(
+                        boost::beast::http::status::internal_server_error);
+                    return;
+                }
+                for (const auto& type : data.items())
+                {
+                    if (!type.value().is_object())
+                    {
+                        BMCWEB_LOG_ERROR << "Illegal Type " << type.key();
+                        messages::addMessageToErrorJson(
+                            response->res.jsonValue,
+                            messages::propertyValueFormatError(type.value(),
+                                                               type.key()));
+                        response->res.result(
+                            boost::beast::http::status::bad_request);
+                        return;
+                    }
+                    for (const auto& record : type.value().items())
+                    {
+                        const std::string& name = record.key();
+                        auto pathItr =
+                            std::find_if(managedObj.begin(), managedObj.end(),
+                                         [&name](const auto& obj) {
+                                             return boost::algorithm::ends_with(
+                                                 obj.first.str, name);
+                                         });
+                        boost::container::flat_map<
+                            std::string, dbus::utility::DbusVariantType>
+                            output;
+
+                        output.reserve(16); // The pid interface length
+
+                        // determines if we're patching entity-manager or
+                        // creating a new object
+                        bool createNewObject = (pathItr == managedObj.end());
+                        if (type.key() == "PidControllers" ||
+                            type.key() == "FanControllers")
+                        {
+                            if (!createNewObject &&
+                                pathItr->second.find(pidConfigurationIface) ==
+                                    pathItr->second.end())
+                            {
+                                createNewObject = true;
+                            }
+                        }
+                        else if (!createNewObject &&
+                                 pathItr->second.find(
+                                     pidZoneConfigurationIface) ==
+                                     pathItr->second.end())
+                        {
+                            createNewObject = true;
+                        }
+                        output["Name"] =
+                            boost::replace_all_copy(name, "_", " ");
+
+                        std::string chassis;
+                        CreatePIDRet ret = createPidInterface(
+                            response, type.key(), record.value(),
+                            pathItr->first.str, managedObj, createNewObject,
+                            output, chassis);
+                        if (ret == CreatePIDRet::fail)
+                        {
+                            return;
+                        }
+                        else if (ret == CreatePIDRet::del)
+                        {
+                            continue;
+                        }
+
+                        if (!createNewObject)
+                        {
+                            for (const auto& property : output)
+                            {
+                                const char* iface =
+                                    type.key() == "FanZones"
+                                        ? pidZoneConfigurationIface
+                                        : pidConfigurationIface;
+                                crow::connections::systemBus->async_method_call(
+                                    [response,
+                                     propertyName{std::string(property.first)}](
+                                        const boost::system::error_code ec) {
+                                        if (ec)
+                                        {
+                                            BMCWEB_LOG_ERROR
+                                                << "Error patching "
+                                                << propertyName << ": " << ec;
+                                            response->res.result(
+                                                boost::beast::http::status::
+                                                    internal_server_error);
+                                        }
+                                    },
+                                    "xyz.openbmc_project.EntityManager",
+                                    pathItr->first.str,
+                                    "org.freedesktop.DBus.Properties", "Set",
+                                    std::string(iface), property.first,
+                                    property.second);
+                            }
+                        }
+                        else
+                        {
+                            if (chassis.empty())
+                            {
+                                BMCWEB_LOG_ERROR
+                                    << "Failed to get chassis from config";
+                                response->res.result(
+                                    boost::beast::http::status::bad_request);
+                                return;
+                            }
+
+                            bool foundChassis = false;
+                            for (const auto& obj : managedObj)
+                            {
+                                if (boost::algorithm::ends_with(obj.first.str,
+                                                                chassis))
+                                {
+                                    chassis = obj.first.str;
+                                    foundChassis = true;
+                                    break;
+                                }
+                            }
+                            if (!foundChassis)
+                            {
+                                BMCWEB_LOG_ERROR
+                                    << "Failed to find chassis on dbus";
+                                messages::addMessageToErrorJson(
+                                    response->res.jsonValue,
+                                    messages::resourceMissingAtURI(
+                                        "/redfish/v1/Chassis/" + chassis));
+                                response->res.result(
+                                    boost::beast::http::status::
+                                        internal_server_error);
+                                return;
+                            }
+
+                            crow::connections::systemBus->async_method_call(
+                                [response](const boost::system::error_code ec) {
+                                    if (ec)
+                                    {
+                                        BMCWEB_LOG_ERROR
+                                            << "Error Adding Pid Object " << ec;
+                                        response->res.result(
+                                            boost::beast::http::status::
+                                                internal_server_error);
+                                    }
+                                },
+                                "xyz.openbmc_project.EntityManager", chassis,
+                                "xyz.openbmc_project.AddObject", "AddObject",
+                                output);
+                        }
+                    }
+                }
+            },
+            "xyz.openbmc_project.EntityManager", "/", objectManagerIface,
+            "GetManagedObjects");
+    }
 
     void doPatch(crow::Response& res, const crow::Request& req,
                  const std::vector<std::string>& params) override
     {
+        nlohmann::json patch;
+        if (!json_util::processJsonFromRequest(res, req, patch))
+        {
+            return;
+        }
+        std::shared_ptr<AsyncResp> response = std::make_shared<AsyncResp>(res);
+        for (const auto& topLevel : patch.items())
+        {
+            if (topLevel.key() == "Oem")
+            {
+                if (!topLevel.value().is_object())
+                {
+                    BMCWEB_LOG_ERROR << "Bad Patch " << topLevel.key();
+                    res.result(boost::beast::http::status::bad_request);
+                    return;
+                }
+            }
+            else
+            {
+                BMCWEB_LOG_ERROR << "Bad Patch " << topLevel.key();
+                messages::addMessageToErrorJson(
+                    response->res.jsonValue,
+                    messages::propertyUnknown(topLevel.key()));
+                res.result(boost::beast::http::status::bad_request);
+                return;
+            }
+            for (const auto& oemLevel : topLevel.value().items())
+            {
+                if (oemLevel.key() == "OpenBmc")
+                {
+                    if (!oemLevel.value().is_object())
+                    {
+                        BMCWEB_LOG_ERROR << "Bad Patch " << oemLevel.key();
+                        res.result(boost::beast::http::status::bad_request);
+                        return;
+                    }
+                    for (const auto& typeLevel : oemLevel.value().items())
+                    {
+
+                        if (typeLevel.key() == "Fan")
+                        {
+                            if (!typeLevel.value().is_object())
+                            {
+                                BMCWEB_LOG_ERROR << "Bad Patch "
+                                                 << typeLevel.key();
+                                messages::addMessageToErrorJson(
+                                    response->res.jsonValue,
+                                    messages::propertyValueFormatError(
+                                        typeLevel.value().dump(),
+                                        typeLevel.key()));
+                                res.result(
+                                    boost::beast::http::status::bad_request);
+                                return;
+                            }
+                            setPidValues(response,
+                                         std::move(typeLevel.value()));
+                        }
+                        else
+                        {
+                            BMCWEB_LOG_ERROR << "Bad Patch " << typeLevel.key();
+                            messages::addMessageToErrorJson(
+                                response->res.jsonValue,
+                                messages::propertyUnknown(typeLevel.key()));
+                            res.result(boost::beast::http::status::bad_request);
+                            return;
+                        }
+                    }
+                }
+                else
+                {
+                    BMCWEB_LOG_ERROR << "Bad Patch " << oemLevel.key();
+                    messages::addMessageToErrorJson(
+                        response->res.jsonValue,
+                        messages::propertyUnknown(oemLevel.key()));
+                    res.result(boost::beast::http::status::bad_request);
+                    return;
+                }
+            }
+        }
     }
 
     std::string getDateTime() const
@@ -483,8 +1017,8 @@
     void doGet(crow::Response& res, const crow::Request& req,
                const std::vector<std::string>& params) override
     {
-        // Collections don't include the static data added by SubRoute because
-        // it has a duplicate entry for members
+        // Collections don't include the static data added by SubRoute
+        // because it has a duplicate entry for members
         res.jsonValue["@odata.id"] = "/redfish/v1/Managers";
         res.jsonValue["@odata.type"] = "#ManagerCollection.ManagerCollection";
         res.jsonValue["@odata.context"] =