Add ServiceIdentification

Implements GET and PATCH support for ServiceIdentification in
Managers/bmc and service root.

Tested:
- Refish Service Validator passes
- Tested on romulus:
1. GET initial value
```
curl -k "https://$BMC/redfish/v1"
{
  ...
}
```
ServiceIdentification is not yet present in service root,
as expected
```
curl -k -H "X-Auth-Token: $XAUTH_TOKEN" "https://$BMC/redfish/v1/Managers/bmc"
{
  ...
  "ServiceIdentification": "",
  ...
}
```

2. PATCH and GET with valid value
```
curl -k -X PATCH "https://$BMC/redfish/v1/Managers/bmc" -H "X-Auth-Token: $XAUTH_TOKEN" \
    -H 'Content-Type: application/json' --data-raw '{"ServiceIdentification": "foo"}'
{
  "@Message.ExtendedInfo": [
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The request completed successfully.",
      "MessageArgs": [],
      "MessageId": "Base.1.19.Success",
      "MessageSeverity": "OK",
      "Resolution": "None."
    }
  ]
}

curl -k "https://$BMC/redfish/v1"
{
  ...
  "ServiceIdentification": "foo",
  ...
}

curl -k -H "X-Auth-Token: $XAUTH_TOKEN" "https://$BMC/redfish/v1/Managers/bmc"
{
  ...
  "ServiceIdentification": "foo",
  ...
}
```

3. PATCH and GET with invalid value
```
curl -k -X PATCH "https://$BMC/redfish/v1/Managers/bmc" -H "X-Auth-Token: $XAUTH_TOKEN" \
    -H 'Content-Type: application/json' --data-raw '{"ServiceIdentification": "$$$"}'
{
  "ServiceIdentification@Message.ExtendedInfo": [
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The value provided for the property ServiceIdentification is not valid.",
      "MessageArgs": [
        "ServiceIdentification"
      ],
      "MessageId": "Base.1.19.PropertyValueError",
      "MessageSeverity": "Warning",
      "Resolution": "Correct the value for the property in the request body and resubmit the request if the operation failed."
    }
  ]
}

curl -k -X PATCH "https://$BMC/redfish/v1/Managers/bmc" -H "X-Auth-Token: $XAUTH_TOKEN" \
    -H 'Content-Type: application/json' --data-raw '{"ServiceIdentification": "2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222"}'
{
  "error": {
    "@Message.ExtendedInfo": [
      {
        "@odata.type": "#Message.v1_1_1.Message",
        "Message": "The string 'ServiceIdentification' exceeds the length limit 99.",
        "MessageArgs": [
          "ServiceIdentification",
          "99"
        ],
        "MessageId": "Base.1.19.StringValueTooLong",
        "MessageSeverity": "Warning",
        "Resolution": "Resubmit the request with an appropriate string length."
      }
    ],
    "code": "Base.1.19.StringValueTooLong",
    "message": "The string 'ServiceIdentification' exceeds the length limit 99."
  }
}

curl -k "https://$BMC/redfish/v1"
{
  ...
  "ServiceIdentification": "foo",
  ...
}

curl -k -H "X-Auth-Token: $XAUTH_TOKEN" "https://$BMC/redfish/v1/Managers/bmc"
{
  ...
  "ServiceIdentification": "foo",
  ...
}
```

Change-Id: I5b71a73e947ec64cabb8d93c8503a18fb43b8937
Signed-off-by: Corey Ethington <cethington@coreweave.com>
diff --git a/docs/Redfish.md b/docs/Redfish.md
index 252507b..20ed111 100644
--- a/docs/Redfish.md
+++ b/docs/Redfish.md
@@ -41,6 +41,7 @@
 - Managers
 - RedfishVersion
 - Registries
+- ServiceIdentification
 - SessionService
 - Systems
 - Tasks
@@ -565,6 +566,7 @@
 - PowerState
 - SerialNumber
 - ServiceEntryPointUUID
+- ServiceIdentification
 - SparePartNumber
 - Status
 - UUID
diff --git a/include/persistent_data.hpp b/include/persistent_data.hpp
index efdfcfa..d210800 100644
--- a/include/persistent_data.hpp
+++ b/include/persistent_data.hpp
@@ -104,6 +104,15 @@
                             systemUuid = *jSystemUuid;
                         }
                     }
+                    else if (item.first == "service_identification")
+                    {
+                        const std::string* jServiceIdentification =
+                            item.second.get_ptr<const std::string*>();
+                        if (jServiceIdentification != nullptr)
+                        {
+                            serviceIdentification = *jServiceIdentification;
+                        }
+                    }
                     else if (item.first == "auth_config")
                     {
                         const nlohmann::json::object_t* jObj =
@@ -298,6 +307,7 @@
             eventServiceConfig.retryTimeoutInterval;
 
         data["system_uuid"] = systemUuid;
+        data["service_identification"] = serviceIdentification;
         data["revision"] = jsonRevision;
         data["timeout"] = SessionStore::getInstance().getTimeoutInSeconds();
 
@@ -383,6 +393,7 @@
     }
 
     std::string systemUuid;
+    std::string serviceIdentification;
 };
 
 inline ConfigFile& getConfig()
diff --git a/redfish-core/include/utils/manager_utils.hpp b/redfish-core/include/utils/manager_utils.hpp
new file mode 100644
index 0000000..a58f357
--- /dev/null
+++ b/redfish-core/include/utils/manager_utils.hpp
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Copyright OpenBMC Authors
+#pragma once
+
+#include "async_resp.hpp"
+#include "error_messages.hpp"
+#include "persistent_data.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <memory>
+#include <string_view>
+
+namespace redfish
+{
+
+namespace manager_utils
+{
+
+inline void setServiceIdentification(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    std::string_view serviceIdentification)
+{
+    constexpr const size_t maxStrSize = 99;
+    constexpr const char* allowedChars =
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 _-";
+    if (serviceIdentification.size() > maxStrSize)
+    {
+        messages::stringValueTooLong(asyncResp->res, "ServiceIdentification",
+                                     maxStrSize);
+        return;
+    }
+    if (serviceIdentification.find_first_not_of(allowedChars) !=
+        std::string_view::npos)
+    {
+        messages::propertyValueError(asyncResp->res, "ServiceIdentification");
+        return;
+    }
+
+    persistent_data::ConfigFile& config = persistent_data::getConfig();
+    config.serviceIdentification = serviceIdentification;
+    config.writeData();
+    messages::success(asyncResp->res);
+}
+
+inline void getServiceIdentification(
+    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+    const bool isServiceRoot)
+{
+    std::string_view serviceIdentification =
+        persistent_data::getConfig().serviceIdentification;
+
+    // This property shall not be present if its value is an empty string or
+    // null: Redfish Data Model Specification 6.125.3
+    if (isServiceRoot && serviceIdentification.empty())
+    {
+        return;
+    }
+    asyncResp->res.jsonValue["ServiceIdentification"] = serviceIdentification;
+}
+
+} // namespace manager_utils
+
+} // namespace redfish
diff --git a/redfish-core/lib/managers.hpp b/redfish-core/lib/managers.hpp
index 40848f4..3b6be77 100644
--- a/redfish-core/lib/managers.hpp
+++ b/redfish-core/lib/managers.hpp
@@ -23,6 +23,7 @@
 #include "registries/privilege_registry.hpp"
 #include "utils/dbus_utils.hpp"
 #include "utils/json_utils.hpp"
+#include "utils/manager_utils.hpp"
 #include "utils/sw_utils.hpp"
 #include "utils/systemd_utils.hpp"
 #include "utils/time_utils.hpp"
@@ -796,7 +797,7 @@
             asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
                 "/redfish/v1/Managers/{}", BMCWEB_REDFISH_MANAGER_URI_NAME);
             asyncResp->res.jsonValue["@odata.type"] =
-                "#Manager.v1_14_0.Manager";
+                "#Manager.v1_15_0.Manager";
             asyncResp->res.jsonValue["Id"] = BMCWEB_REDFISH_MANAGER_URI_NAME;
             asyncResp->res.jsonValue["Name"] = "OpenBmc Manager";
             asyncResp->res.jsonValue["Description"] =
@@ -820,6 +821,8 @@
                     "/redfish/v1/Managers/{}/EthernetInterfaces",
                     BMCWEB_REDFISH_MANAGER_URI_NAME);
 
+            manager_utils::getServiceIdentification(asyncResp, false);
+
             if constexpr (BMCWEB_VM_NBDPROXY)
             {
                 asyncResp->res.jsonValue["VirtualMedia"]["@odata.id"] =
@@ -964,6 +967,7 @@
                 std::optional<nlohmann::json::object_t> fanZones;
                 std::optional<nlohmann::json::object_t> stepwiseControllers;
                 std::optional<std::string> profile;
+                std::optional<std::string> serviceIdentification;
 
                 if (!json_util::readJsonPatch(                            //
                         req, asyncResp->res,                              //
@@ -977,7 +981,8 @@
                         "Oem/OpenBmc/Fan/PidControllers", pidControllers, //
                         "Oem/OpenBmc/Fan/Profile", profile,               //
                         "Oem/OpenBmc/Fan/StepwiseControllers",
-                        stepwiseControllers                               //
+                        stepwiseControllers,                              //
+                        "ServiceIdentification", serviceIdentification    //
                         ))
                 {
                     return;
@@ -1000,6 +1005,12 @@
                         asyncResp, *locationIndicatorActive, managerId);
                 }
 
+                if (serviceIdentification)
+                {
+                    manager_utils::setServiceIdentification(
+                        asyncResp, serviceIdentification.value());
+                }
+
                 RedfishService::getInstance(app).handleSubRoute(req, asyncResp);
             });
 }
diff --git a/redfish-core/lib/service_root.hpp b/redfish-core/lib/service_root.hpp
index 34f0e31..a67a75d 100644
--- a/redfish-core/lib/service_root.hpp
+++ b/redfish-core/lib/service_root.hpp
@@ -11,6 +11,7 @@
 #include "persistent_data.hpp"
 #include "query.hpp"
 #include "registries/privilege_registry.hpp"
+#include "utils/manager_utils.hpp"
 
 #include <boost/beast/http/field.hpp>
 #include <boost/beast/http/verb.hpp>
@@ -80,6 +81,7 @@
         "/redfish/v1/EventService";
     asyncResp->res.jsonValue["TelemetryService"]["@odata.id"] =
         "/redfish/v1/TelemetryService";
+    manager_utils::getServiceIdentification(asyncResp, true);
     asyncResp->res.jsonValue["Cables"]["@odata.id"] = "/redfish/v1/Cables";
 
     asyncResp->res.jsonValue["Links"]["ManagerProvidingService"]["@odata.id"] =