xref: /openbmc/bmcweb/redfish-core/lib/event_service.hpp (revision 3577e44683a5ade8ad02a6418984b56f4ca2bcac)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 // SPDX-FileCopyrightText: Copyright 2020 Intel Corporation
4 #pragma once
5 #include "app.hpp"
6 #include "async_resp.hpp"
7 #include "dbus_utility.hpp"
8 #include "error_messages.hpp"
9 #include "event_service_manager.hpp"
10 #include "event_service_store.hpp"
11 #include "generated/enums/event_destination.hpp"
12 #include "http/utility.hpp"
13 #include "http_request.hpp"
14 #include "io_context_singleton.hpp"
15 #include "logging.hpp"
16 #include "query.hpp"
17 #include "registries.hpp"
18 #include "registries/privilege_registry.hpp"
19 #include "snmp_trap_event_clients.hpp"
20 #include "subscription.hpp"
21 #include "utils/json_utils.hpp"
22 
23 #include <asm-generic/errno.h>
24 
25 #include <boost/beast/http/status.hpp>
26 #include <boost/beast/http/verb.hpp>
27 #include <boost/system/error_code.hpp>
28 #include <boost/system/result.hpp>
29 #include <boost/url/format.hpp>
30 #include <boost/url/parse.hpp>
31 #include <boost/url/url.hpp>
32 #include <sdbusplus/message/native_types.hpp>
33 
34 #include <algorithm>
35 #include <array>
36 #include <cerrno>
37 #include <cstddef>
38 #include <cstdint>
39 #include <memory>
40 #include <optional>
41 #include <ranges>
42 #include <string>
43 #include <utility>
44 #include <vector>
45 
46 namespace redfish
47 {
48 
49 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = {
50     eventFormatType, metricReportFormatType};
51 static constexpr const std::array<const char*, 4> supportedRegPrefixes = {
52     "Base", "OpenBMC", "TaskEvent", "HeartbeatEvent"};
53 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
54     "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
55 
56 static constexpr const std::array<const char*, 2> supportedResourceTypes = {
57     "Task", "Heartbeat"};
58 
requestRoutesEventService(App & app)59 inline void requestRoutesEventService(App& app)
60 {
61     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
62         .privileges(redfish::privileges::getEventService)
63         .methods(
64             boost::beast::http::verb::
65                 get)([&app](
66                          const crow::Request& req,
67                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
68             if (!redfish::setUpRedfishRoute(app, req, asyncResp))
69             {
70                 return;
71             }
72 
73             asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/EventService";
74             asyncResp->res.jsonValue["@odata.type"] =
75                 "#EventService.v1_5_0.EventService";
76             asyncResp->res.jsonValue["Id"] = "EventService";
77             asyncResp->res.jsonValue["Name"] = "Event Service";
78             asyncResp->res.jsonValue["ServerSentEventUri"] =
79                 "/redfish/v1/EventService/SSE";
80 
81             asyncResp->res.jsonValue["Subscriptions"]["@odata.id"] =
82                 "/redfish/v1/EventService/Subscriptions";
83             asyncResp->res.jsonValue["Actions"]["#EventService.SubmitTestEvent"]
84                                     ["target"] =
85                 "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent";
86 
87             const persistent_data::EventServiceConfig eventServiceConfig =
88                 persistent_data::EventServiceStore::getInstance()
89                     .getEventServiceConfig();
90 
91             asyncResp->res.jsonValue["Status"]["State"] =
92                 (eventServiceConfig.enabled ? "Enabled" : "Disabled");
93             asyncResp->res.jsonValue["ServiceEnabled"] =
94                 eventServiceConfig.enabled;
95             asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
96                 eventServiceConfig.retryAttempts;
97             asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
98                 eventServiceConfig.retryTimeoutInterval;
99             asyncResp->res.jsonValue["EventFormatTypes"] =
100                 supportedEvtFormatTypes;
101             asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
102             asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes;
103 
104             nlohmann::json::object_t supportedSSEFilters;
105             supportedSSEFilters["EventFormatType"] = true;
106             supportedSSEFilters["MessageId"] = true;
107             supportedSSEFilters["MetricReportDefinition"] = true;
108             supportedSSEFilters["RegistryPrefix"] = true;
109             supportedSSEFilters["OriginResource"] = false;
110             supportedSSEFilters["ResourceType"] = false;
111 
112             asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
113                 std::move(supportedSSEFilters);
114         });
115 
116     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
117         .privileges(redfish::privileges::patchEventService)
118         .methods(boost::beast::http::verb::patch)(
119             [&app](const crow::Request& req,
120                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
121                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
122                 {
123                     return;
124                 }
125                 std::optional<bool> serviceEnabled;
126                 std::optional<uint32_t> retryAttemps;
127                 std::optional<uint32_t> retryInterval;
128                 if (!json_util::readJsonPatch(                         //
129                         req, asyncResp->res,                           //
130                         "DeliveryRetryAttempts", retryAttemps,         //
131                         "DeliveryRetryIntervalSeconds", retryInterval, //
132                         "ServiceEnabled", serviceEnabled               //
133                         ))
134                 {
135                     return;
136                 }
137 
138                 persistent_data::EventServiceConfig eventServiceConfig =
139                     persistent_data::EventServiceStore::getInstance()
140                         .getEventServiceConfig();
141 
142                 if (serviceEnabled)
143                 {
144                     eventServiceConfig.enabled = *serviceEnabled;
145                 }
146 
147                 if (retryAttemps)
148                 {
149                     // Supported range [1-3]
150                     if ((*retryAttemps < 1) || (*retryAttemps > 3))
151                     {
152                         messages::queryParameterOutOfRange(
153                             asyncResp->res, std::to_string(*retryAttemps),
154                             "DeliveryRetryAttempts", "[1-3]");
155                     }
156                     else
157                     {
158                         eventServiceConfig.retryAttempts = *retryAttemps;
159                     }
160                 }
161 
162                 if (retryInterval)
163                 {
164                     // Supported range [5 - 180]
165                     if ((*retryInterval < 5) || (*retryInterval > 180))
166                     {
167                         messages::queryParameterOutOfRange(
168                             asyncResp->res, std::to_string(*retryInterval),
169                             "DeliveryRetryIntervalSeconds", "[5-180]");
170                     }
171                     else
172                     {
173                         eventServiceConfig.retryTimeoutInterval =
174                             *retryInterval;
175                     }
176                 }
177 
178                 EventServiceManager::getInstance().setEventServiceConfig(
179                     eventServiceConfig);
180             });
181 }
182 
requestRoutesSubmitTestEvent(App & app)183 inline void requestRoutesSubmitTestEvent(App& app)
184 {
185     BMCWEB_ROUTE(
186         app, "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
187         .privileges(redfish::privileges::postEventService)
188         .methods(boost::beast::http::verb::post)(
189             [&app](const crow::Request& req,
190                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
191                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
192                 {
193                     return;
194                 }
195 
196                 // From the Redfish spec on EventId
197                 // A service can ignore this value and replace it with its own.
198                 // note that this parameter is intentionally ignored
199 
200                 std::optional<std::string> eventId;
201                 TestEvent testEvent;
202                 // clang-format off
203                 if (!json_util::readJsonAction(
204                         req, asyncResp->res,
205                         "EventGroupId", testEvent.eventGroupId,
206                         "EventId", eventId,
207                         "EventTimestamp", testEvent.eventTimestamp,
208                         "Message", testEvent.message,
209                         "MessageArgs", testEvent.messageArgs,
210                         "MessageId", testEvent.messageId,
211                         "OriginOfCondition", testEvent.originOfCondition,
212                         "Resolution", testEvent.resolution,
213                         "Severity", testEvent.severity))
214                 {
215                     return;
216                 }
217                 // clang-format on
218 
219                 if (!EventServiceManager::getInstance().sendTestEventLog(
220                         testEvent))
221                 {
222                     messages::serviceDisabled(asyncResp->res,
223                                               "/redfish/v1/EventService/");
224                     return;
225                 }
226                 asyncResp->res.result(boost::beast::http::status::no_content);
227             });
228 }
229 
doSubscriptionCollection(const boost::system::error_code & ec,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const dbus::utility::ManagedObjectType & resp)230 inline void doSubscriptionCollection(
231     const boost::system::error_code& ec,
232     const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
233     const dbus::utility::ManagedObjectType& resp)
234 {
235     if (ec)
236     {
237         if (ec.value() == EBADR || ec.value() == EHOSTUNREACH)
238         {
239             // This is an optional process so just return if it isn't there
240             return;
241         }
242 
243         BMCWEB_LOG_ERROR("D-Bus response error on GetManagedObjects {}", ec);
244         messages::internalError(asyncResp->res);
245         return;
246     }
247     nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
248     for (const auto& objpath : resp)
249     {
250         sdbusplus::message::object_path path(objpath.first);
251         const std::string snmpId = path.filename();
252         if (snmpId.empty())
253         {
254             BMCWEB_LOG_ERROR("The SNMP client ID is wrong");
255             messages::internalError(asyncResp->res);
256             return;
257         }
258 
259         getSnmpSubscriptionList(asyncResp, snmpId, memberArray);
260     }
261 }
262 
requestRoutesEventDestinationCollection(App & app)263 inline void requestRoutesEventDestinationCollection(App& app)
264 {
265     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
266         .privileges(redfish::privileges::getEventDestinationCollection)
267         .methods(boost::beast::http::verb::get)(
268             [&app](const crow::Request& req,
269                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
270                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
271                 {
272                     return;
273                 }
274                 asyncResp->res.jsonValue["@odata.type"] =
275                     "#EventDestinationCollection.EventDestinationCollection";
276                 asyncResp->res.jsonValue["@odata.id"] =
277                     "/redfish/v1/EventService/Subscriptions";
278                 asyncResp->res.jsonValue["Name"] =
279                     "Event Destination Collections";
280 
281                 nlohmann::json& memberArray =
282                     asyncResp->res.jsonValue["Members"];
283 
284                 std::vector<std::string> subscripIds =
285                     EventServiceManager::getInstance().getAllIDs();
286                 memberArray = nlohmann::json::array();
287                 asyncResp->res.jsonValue["Members@odata.count"] =
288                     subscripIds.size();
289 
290                 for (const std::string& id : subscripIds)
291                 {
292                     nlohmann::json::object_t member;
293                     member["@odata.id"] = boost::urls::format(
294                         "/redfish/v1/EventService/Subscriptions/{}" + id);
295                     memberArray.emplace_back(std::move(member));
296                 }
297                 dbus::utility::async_method_call(
298                     asyncResp,
299                     [asyncResp](const boost::system::error_code& ec,
300                                 const dbus::utility::ManagedObjectType& resp) {
301                         doSubscriptionCollection(ec, asyncResp, resp);
302                     },
303                     "xyz.openbmc_project.Network.SNMP",
304                     "/xyz/openbmc_project/network/snmp/manager",
305                     "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
306             });
307 
308     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
309         .privileges(redfish::privileges::postEventDestinationCollection)
310         .methods(
311             boost::beast::http::verb::
312                 post)([&app](
313                           const crow::Request& req,
314                           const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
315             if (!redfish::setUpRedfishRoute(app, req, asyncResp))
316             {
317                 return;
318             }
319             if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
320                 maxNoOfSubscriptions)
321             {
322                 messages::eventSubscriptionLimitExceeded(asyncResp->res);
323                 return;
324             }
325             std::string destUrl;
326             std::string protocol;
327             std::optional<bool> verifyCertificate;
328             std::optional<std::string> context;
329             std::optional<std::string> subscriptionType;
330             std::optional<std::string> eventFormatType2;
331             std::optional<std::string> retryPolicy;
332             std::optional<bool> sendHeartbeat;
333             std::optional<uint64_t> hbIntervalMinutes;
334             std::optional<std::vector<std::string>> msgIds;
335             std::optional<std::vector<std::string>> regPrefixes;
336             std::optional<std::vector<std::string>> originResources;
337             std::optional<std::vector<std::string>> resTypes;
338             std::optional<std::vector<nlohmann::json::object_t>> headers;
339             std::optional<std::vector<nlohmann::json::object_t>> mrdJsonArray;
340 
341             if (!json_util::readJsonPatch(                         //
342                     req, asyncResp->res,                           //
343                     "Context", context,                            //
344                     "DeliveryRetryPolicy", retryPolicy,            //
345                     "Destination", destUrl,                        //
346                     "EventFormatType", eventFormatType2,           //
347                     "HeartbeatIntervalMinutes", hbIntervalMinutes, //
348                     "HttpHeaders", headers,                        //
349                     "MessageIds", msgIds,                          //
350                     "MetricReportDefinitions", mrdJsonArray,       //
351                     "OriginResources", originResources,            //
352                     "Protocol", protocol,                          //
353                     "RegistryPrefixes", regPrefixes,               //
354                     "ResourceTypes", resTypes,                     //
355                     "SendHeartbeat", sendHeartbeat,                //
356                     "SubscriptionType", subscriptionType,          //
357                     "VerifyCertificate", verifyCertificate         //
358                     ))
359             {
360                 return;
361             }
362             // clang-format on
363 
364             // https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
365             static constexpr const uint16_t maxDestinationSize = 2000;
366             if (destUrl.size() > maxDestinationSize)
367             {
368                 messages::stringValueTooLong(asyncResp->res, "Destination",
369                                              maxDestinationSize);
370                 return;
371             }
372 
373             if (regPrefixes && msgIds)
374             {
375                 if (!regPrefixes->empty() && !msgIds->empty())
376                 {
377                     messages::propertyValueConflict(
378                         asyncResp->res, "MessageIds", "RegistryPrefixes");
379                     return;
380                 }
381             }
382 
383             boost::system::result<boost::urls::url> url =
384                 boost::urls::parse_absolute_uri(destUrl);
385             if (!url)
386             {
387                 BMCWEB_LOG_WARNING(
388                     "Failed to validate and split destination url");
389                 messages::propertyValueFormatError(asyncResp->res, destUrl,
390                                                    "Destination");
391                 return;
392             }
393             url->normalize();
394 
395             // port_number returns zero if it is not a valid representable port
396             if (url->has_port() && url->port_number() == 0)
397             {
398                 BMCWEB_LOG_WARNING("{} is an invalid port in destination url",
399                                    url->port());
400                 messages::propertyValueFormatError(asyncResp->res, destUrl,
401                                                    "Destination");
402                 return;
403             }
404 
405             crow::utility::setProtocolDefaults(*url, protocol);
406             crow::utility::setPortDefaults(*url);
407 
408             if (url->path().empty())
409             {
410                 url->set_path("/");
411             }
412 
413             if (url->has_userinfo())
414             {
415                 messages::propertyValueFormatError(asyncResp->res, destUrl,
416                                                    "Destination");
417                 return;
418             }
419 
420             if (protocol == "SNMPv2c")
421             {
422                 if (context)
423                 {
424                     messages::propertyValueConflict(asyncResp->res, "Context",
425                                                     "Protocol");
426                     return;
427                 }
428                 if (eventFormatType2)
429                 {
430                     messages::propertyValueConflict(
431                         asyncResp->res, "EventFormatType", "Protocol");
432                     return;
433                 }
434                 if (retryPolicy)
435                 {
436                     messages::propertyValueConflict(asyncResp->res,
437                                                     "RetryPolicy", "Protocol");
438                     return;
439                 }
440                 if (sendHeartbeat)
441                 {
442                     messages::propertyValueConflict(
443                         asyncResp->res, "SendHeartbeat", "Protocol");
444                     return;
445                 }
446                 if (hbIntervalMinutes)
447                 {
448                     messages::propertyValueConflict(
449                         asyncResp->res, "HeartbeatIntervalMinutes", "Protocol");
450                     return;
451                 }
452                 if (msgIds)
453                 {
454                     messages::propertyValueConflict(asyncResp->res,
455                                                     "MessageIds", "Protocol");
456                     return;
457                 }
458                 if (regPrefixes)
459                 {
460                     messages::propertyValueConflict(
461                         asyncResp->res, "RegistryPrefixes", "Protocol");
462                     return;
463                 }
464                 if (resTypes)
465                 {
466                     messages::propertyValueConflict(
467                         asyncResp->res, "ResourceTypes", "Protocol");
468                     return;
469                 }
470                 if (headers)
471                 {
472                     messages::propertyValueConflict(asyncResp->res,
473                                                     "HttpHeaders", "Protocol");
474                     return;
475                 }
476                 if (mrdJsonArray)
477                 {
478                     messages::propertyValueConflict(
479                         asyncResp->res, "MetricReportDefinitions", "Protocol");
480                     return;
481                 }
482                 if (url->scheme() != "snmp")
483                 {
484                     messages::propertyValueConflict(asyncResp->res,
485                                                     "Destination", "Protocol");
486                     return;
487                 }
488 
489                 addSnmpTrapClient(asyncResp, url->host_address(),
490                                   url->port_number());
491                 return;
492             }
493 
494             std::shared_ptr<Subscription> subValue =
495                 std::make_shared<Subscription>(
496                     std::make_shared<persistent_data::UserSubscription>(), *url,
497                     getIoContext());
498 
499             if (subscriptionType)
500             {
501                 if (*subscriptionType != "RedfishEvent")
502                 {
503                     messages::propertyValueNotInList(
504                         asyncResp->res, *subscriptionType, "SubscriptionType");
505                     return;
506                 }
507                 subValue->userSub->subscriptionType = *subscriptionType;
508             }
509             else
510             {
511                 // Default
512                 subValue->userSub->subscriptionType = "RedfishEvent";
513             }
514 
515             if (protocol != "Redfish")
516             {
517                 messages::propertyValueNotInList(asyncResp->res, protocol,
518                                                  "Protocol");
519                 return;
520             }
521             subValue->userSub->protocol = protocol;
522 
523             if (verifyCertificate)
524             {
525                 subValue->userSub->verifyCertificate = *verifyCertificate;
526             }
527 
528             if (eventFormatType2)
529             {
530                 if (std::ranges::find(supportedEvtFormatTypes,
531                                       *eventFormatType2) ==
532                     supportedEvtFormatTypes.end())
533                 {
534                     messages::propertyValueNotInList(
535                         asyncResp->res, *eventFormatType2, "EventFormatType");
536                     return;
537                 }
538                 subValue->userSub->eventFormatType = *eventFormatType2;
539             }
540             else
541             {
542                 // If not specified, use default "Event"
543                 subValue->userSub->eventFormatType = "Event";
544             }
545 
546             if (context)
547             {
548                 // This value is selected arbitrarily.
549                 constexpr const size_t maxContextSize = 256;
550                 if (context->size() > maxContextSize)
551                 {
552                     messages::stringValueTooLong(asyncResp->res, "Context",
553                                                  maxContextSize);
554                     return;
555                 }
556                 subValue->userSub->customText = *context;
557             }
558 
559             if (headers)
560             {
561                 size_t cumulativeLen = 0;
562 
563                 for (const nlohmann::json::object_t& headerChunk : *headers)
564                 {
565                     for (const auto& item : headerChunk)
566                     {
567                         const std::string* value =
568                             item.second.get_ptr<const std::string*>();
569                         if (value == nullptr)
570                         {
571                             messages::propertyValueFormatError(
572                                 asyncResp->res, item.second,
573                                 "HttpHeaders/" + item.first);
574                             return;
575                         }
576                         // Adding a new json value is the size of the key, +
577                         // the size of the value + 2 * 2 quotes for each, +
578                         // the colon and space between. example:
579                         // "key": "value"
580                         cumulativeLen += item.first.size() + value->size() + 6;
581                         // This value is selected to mirror http_connection.hpp
582                         constexpr const uint16_t maxHeaderSizeED = 8096;
583                         if (cumulativeLen > maxHeaderSizeED)
584                         {
585                             messages::arraySizeTooLong(
586                                 asyncResp->res, "HttpHeaders", maxHeaderSizeED);
587                             return;
588                         }
589                         subValue->userSub->httpHeaders.set(item.first, *value);
590                     }
591                 }
592             }
593 
594             if (regPrefixes)
595             {
596                 for (const std::string& it : *regPrefixes)
597                 {
598                     if (std::ranges::find(supportedRegPrefixes, it) ==
599                         supportedRegPrefixes.end())
600                     {
601                         messages::propertyValueNotInList(asyncResp->res, it,
602                                                          "RegistryPrefixes");
603                         return;
604                     }
605                 }
606                 subValue->userSub->registryPrefixes = *regPrefixes;
607             }
608 
609             if (originResources)
610             {
611                 subValue->userSub->originResources = *originResources;
612             }
613 
614             if (resTypes)
615             {
616                 for (const std::string& it : *resTypes)
617                 {
618                     if (std::ranges::find(supportedResourceTypes, it) ==
619                         supportedResourceTypes.end())
620                     {
621                         messages::propertyValueNotInList(asyncResp->res, it,
622                                                          "ResourceTypes");
623                         return;
624                     }
625                 }
626                 subValue->userSub->resourceTypes = *resTypes;
627             }
628 
629             if (msgIds)
630             {
631                 std::vector<std::string> registryPrefix;
632 
633                 // If no registry prefixes are mentioned, consider all
634                 // supported prefixes
635                 if (subValue->userSub->registryPrefixes.empty())
636                 {
637                     registryPrefix.assign(supportedRegPrefixes.begin(),
638                                           supportedRegPrefixes.end());
639                 }
640                 else
641                 {
642                     registryPrefix = subValue->userSub->registryPrefixes;
643                 }
644 
645                 for (const std::string& id : *msgIds)
646                 {
647                     bool validId = false;
648 
649                     // Check for Message ID in each of the selected Registry
650                     for (const std::string& it : registryPrefix)
651                     {
652                         const registries::MessageEntries registry =
653                             redfish::registries::getRegistryMessagesFromPrefix(
654                                 it);
655 
656                         if (std::ranges::any_of(
657                                 registry,
658                                 [&id](const redfish::registries::MessageEntry&
659                                           messageEntry) {
660                                     return id == messageEntry.first;
661                                 }))
662                         {
663                             validId = true;
664                             break;
665                         }
666                     }
667 
668                     if (!validId)
669                     {
670                         messages::propertyValueNotInList(asyncResp->res, id,
671                                                          "MessageIds");
672                         return;
673                     }
674                 }
675 
676                 subValue->userSub->registryMsgIds = *msgIds;
677             }
678 
679             if (retryPolicy)
680             {
681                 if (std::ranges::find(supportedRetryPolicies, *retryPolicy) ==
682                     supportedRetryPolicies.end())
683                 {
684                     messages::propertyValueNotInList(
685                         asyncResp->res, *retryPolicy, "DeliveryRetryPolicy");
686                     return;
687                 }
688                 subValue->userSub->retryPolicy = *retryPolicy;
689             }
690             else
691             {
692                 // Default "TerminateAfterRetries"
693                 subValue->userSub->retryPolicy = "TerminateAfterRetries";
694             }
695             if (sendHeartbeat)
696             {
697                 subValue->userSub->sendHeartbeat = *sendHeartbeat;
698             }
699             if (hbIntervalMinutes)
700             {
701                 if (*hbIntervalMinutes < 1 || *hbIntervalMinutes > 65535)
702                 {
703                     messages::propertyValueOutOfRange(
704                         asyncResp->res, *hbIntervalMinutes,
705                         "HeartbeatIntervalMinutes");
706                     return;
707                 }
708                 subValue->userSub->hbIntervalMinutes = *hbIntervalMinutes;
709             }
710 
711             if (mrdJsonArray)
712             {
713                 for (nlohmann::json::object_t& mrdObj : *mrdJsonArray)
714                 {
715                     std::string mrdUri;
716 
717                     if (!json_util::readJsonObject(mrdObj, asyncResp->res,
718                                                    "@odata.id", mrdUri))
719 
720                     {
721                         return;
722                     }
723                     subValue->userSub->metricReportDefinitions.emplace_back(
724                         mrdUri);
725                 }
726             }
727 
728             std::string id =
729                 EventServiceManager::getInstance().addPushSubscription(
730                     subValue);
731             if (id.empty())
732             {
733                 messages::internalError(asyncResp->res);
734                 return;
735             }
736 
737             messages::created(asyncResp->res);
738             asyncResp->res.addHeader(
739                 "Location", "/redfish/v1/EventService/Subscriptions/" + id);
740 
741             // schedule a heartbeat
742             if (subValue->userSub->sendHeartbeat)
743             {
744                 subValue->scheduleNextHeartbeatEvent();
745             }
746         });
747 }
748 
requestRoutesEventDestination(App & app)749 inline void requestRoutesEventDestination(App& app)
750 {
751     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
752         .privileges(redfish::privileges::getEventDestination)
753         .methods(boost::beast::http::verb::get)(
754             [&app](const crow::Request& req,
755                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
756                    const std::string& param) {
757                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
758                 {
759                     return;
760                 }
761 
762                 if (param.starts_with("snmp"))
763                 {
764                     getSnmpTrapClient(asyncResp, param);
765                     return;
766                 }
767 
768                 std::shared_ptr<Subscription> subValue =
769                     EventServiceManager::getInstance().getSubscription(param);
770                 if (subValue == nullptr)
771                 {
772                     asyncResp->res.result(
773                         boost::beast::http::status::not_found);
774                     return;
775                 }
776                 const std::string& id = param;
777 
778                 const persistent_data::UserSubscription& userSub =
779                     *subValue->userSub;
780 
781                 nlohmann::json& jVal = asyncResp->res.jsonValue;
782                 jVal["@odata.type"] =
783                     "#EventDestination.v1_14_1.EventDestination";
784                 jVal["Protocol"] =
785                     event_destination::EventDestinationProtocol::Redfish;
786                 jVal["@odata.id"] = boost::urls::format(
787                     "/redfish/v1/EventService/Subscriptions/{}", id);
788                 jVal["Id"] = id;
789                 jVal["Name"] = "Event Destination " + id;
790                 jVal["Destination"] = userSub.destinationUrl;
791                 jVal["Context"] = userSub.customText;
792                 jVal["SubscriptionType"] = userSub.subscriptionType;
793                 jVal["HttpHeaders"] = nlohmann::json::array();
794                 jVal["EventFormatType"] = userSub.eventFormatType;
795                 jVal["RegistryPrefixes"] = userSub.registryPrefixes;
796                 jVal["ResourceTypes"] = userSub.resourceTypes;
797 
798                 jVal["MessageIds"] = userSub.registryMsgIds;
799                 jVal["DeliveryRetryPolicy"] = userSub.retryPolicy;
800                 jVal["SendHeartbeat"] = userSub.sendHeartbeat;
801                 jVal["HeartbeatIntervalMinutes"] = userSub.hbIntervalMinutes;
802                 jVal["VerifyCertificate"] = userSub.verifyCertificate;
803 
804                 nlohmann::json::array_t mrdJsonArray;
805                 for (const auto& mdrUri : userSub.metricReportDefinitions)
806                 {
807                     nlohmann::json::object_t mdr;
808                     mdr["@odata.id"] = mdrUri;
809                     mrdJsonArray.emplace_back(std::move(mdr));
810                 }
811                 jVal["MetricReportDefinitions"] = mrdJsonArray;
812             });
813     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
814         // The below privilege is wrong, it should be ConfigureManager OR
815         // ConfigureSelf
816         // https://github.com/openbmc/bmcweb/issues/220
817         //.privileges(redfish::privileges::patchEventDestination)
818         .privileges({{"ConfigureManager"}})
819         .methods(boost::beast::http::verb::patch)(
820             [&app](const crow::Request& req,
821                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
822                    const std::string& param) {
823                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
824                 {
825                     return;
826                 }
827                 std::shared_ptr<Subscription> subValue =
828                     EventServiceManager::getInstance().getSubscription(param);
829                 if (subValue == nullptr)
830                 {
831                     asyncResp->res.result(
832                         boost::beast::http::status::not_found);
833                     return;
834                 }
835 
836                 std::optional<std::string> context;
837                 std::optional<std::string> retryPolicy;
838                 std::optional<bool> sendHeartbeat;
839                 std::optional<uint64_t> hbIntervalMinutes;
840                 std::optional<bool> verifyCertificate;
841                 std::optional<std::vector<nlohmann::json::object_t>> headers;
842 
843                 if (!json_util::readJsonPatch(                         //
844                         req, asyncResp->res,                           //
845                         "Context", context,                            //
846                         "DeliveryRetryPolicy", retryPolicy,            //
847                         "HeartbeatIntervalMinutes", hbIntervalMinutes, //
848                         "HttpHeaders", headers,                        //
849                         "SendHeartbeat", sendHeartbeat,                //
850                         "VerifyCertificate", verifyCertificate         //
851                         ))
852                 {
853                     return;
854                 }
855 
856                 if (context)
857                 {
858                     subValue->userSub->customText = *context;
859                 }
860 
861                 if (headers)
862                 {
863                     boost::beast::http::fields fields;
864                     for (const nlohmann::json::object_t& headerChunk : *headers)
865                     {
866                         for (const auto& it : headerChunk)
867                         {
868                             const std::string* value =
869                                 it.second.get_ptr<const std::string*>();
870                             if (value == nullptr)
871                             {
872                                 messages::propertyValueFormatError(
873                                     asyncResp->res, it.second,
874                                     "HttpHeaders/" + it.first);
875                                 return;
876                             }
877                             fields.set(it.first, *value);
878                         }
879                     }
880                     subValue->userSub->httpHeaders = std::move(fields);
881                 }
882 
883                 if (retryPolicy)
884                 {
885                     if (std::ranges::find(supportedRetryPolicies,
886                                           *retryPolicy) ==
887                         supportedRetryPolicies.end())
888                     {
889                         messages::propertyValueNotInList(asyncResp->res,
890                                                          *retryPolicy,
891                                                          "DeliveryRetryPolicy");
892                         return;
893                     }
894                     subValue->userSub->retryPolicy = *retryPolicy;
895                 }
896 
897                 if (sendHeartbeat)
898                 {
899                     subValue->userSub->sendHeartbeat = *sendHeartbeat;
900                 }
901                 if (hbIntervalMinutes)
902                 {
903                     if (*hbIntervalMinutes < 1 || *hbIntervalMinutes > 65535)
904                     {
905                         messages::propertyValueOutOfRange(
906                             asyncResp->res, *hbIntervalMinutes,
907                             "HeartbeatIntervalMinutes");
908                         return;
909                     }
910                     subValue->userSub->hbIntervalMinutes = *hbIntervalMinutes;
911                 }
912 
913                 if (hbIntervalMinutes || sendHeartbeat)
914                 {
915                     // if Heartbeat interval or send heart were changed, cancel
916                     // the heartbeat timer if running and start a new heartbeat
917                     // if needed
918                     subValue->heartbeatParametersChanged();
919                 }
920 
921                 if (verifyCertificate)
922                 {
923                     subValue->userSub->verifyCertificate = *verifyCertificate;
924                 }
925 
926                 EventServiceManager::getInstance().updateSubscriptionData();
927             });
928     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
929         // The below privilege is wrong, it should be ConfigureManager OR
930         // ConfigureSelf
931         // https://github.com/openbmc/bmcweb/issues/220
932         //.privileges(redfish::privileges::deleteEventDestination)
933         .privileges({{"ConfigureManager"}})
934         .methods(boost::beast::http::verb::delete_)(
935             [&app](const crow::Request& req,
936                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
937                    const std::string& param) {
938                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
939                 {
940                     return;
941                 }
942                 EventServiceManager& event = EventServiceManager::getInstance();
943                 if (param.starts_with("snmp"))
944                 {
945                     deleteSnmpTrapClient(asyncResp, param);
946                     event.deleteSubscription(param);
947                     return;
948                 }
949 
950                 if (!event.deleteSubscription(param))
951                 {
952                     messages::resourceNotFound(asyncResp->res,
953                                                "EventDestination", param);
954                     return;
955                 }
956                 messages::success(asyncResp->res);
957             });
958 }
959 
960 } // namespace redfish
961