xref: /openbmc/bmcweb/features/redfish/lib/event_service.hpp (revision 600af5f1ee6f3a589d5c1a7be31579ca705eef71)
1 /*
2 // Copyright (c) 2020 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 #pragma once
17 #include "app.hpp"
18 #include "event_service_manager.hpp"
19 #include "http/utility.hpp"
20 #include "logging.hpp"
21 #include "query.hpp"
22 #include "registries/privilege_registry.hpp"
23 
24 #include <boost/beast/http/fields.hpp>
25 
26 #include <span>
27 
28 namespace redfish
29 {
30 
31 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = {
32     eventFormatType, metricReportFormatType};
33 static constexpr const std::array<const char*, 3> supportedRegPrefixes = {
34     "Base", "OpenBMC", "TaskEvent"};
35 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
36     "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
37 
38 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
39 static constexpr const std::array<const char*, 2> supportedResourceTypes = {
40     "IBMConfigFile", "Task"};
41 #else
42 static constexpr const std::array<const char*, 1> supportedResourceTypes = {
43     "Task"};
44 #endif
45 
46 static constexpr const uint8_t maxNoOfSubscriptions = 20;
47 
48 inline void requestRoutesEventService(App& app)
49 {
50     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
51         .privileges(redfish::privileges::getEventService)
52         .methods(boost::beast::http::verb::get)(
53             [&app](const crow::Request& req,
54                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
55         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
56         {
57             return;
58         }
59 
60         asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/EventService";
61         asyncResp->res.jsonValue["@odata.type"] =
62             "#EventService.v1_5_0.EventService";
63         asyncResp->res.jsonValue["Id"] = "EventService";
64         asyncResp->res.jsonValue["Name"] = "Event Service";
65         asyncResp->res.jsonValue["Subscriptions"]["@odata.id"] =
66             "/redfish/v1/EventService/Subscriptions";
67         asyncResp->res
68             .jsonValue["Actions"]["#EventService.SubmitTestEvent"]["target"] =
69             "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent";
70 
71         const persistent_data::EventServiceConfig eventServiceConfig =
72             persistent_data::EventServiceStore::getInstance()
73                 .getEventServiceConfig();
74 
75         asyncResp->res.jsonValue["Status"]["State"] =
76             (eventServiceConfig.enabled ? "Enabled" : "Disabled");
77         asyncResp->res.jsonValue["ServiceEnabled"] = eventServiceConfig.enabled;
78         asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
79             eventServiceConfig.retryAttempts;
80         asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
81             eventServiceConfig.retryTimeoutInterval;
82         asyncResp->res.jsonValue["EventFormatTypes"] = supportedEvtFormatTypes;
83         asyncResp->res.jsonValue["RegistryPrefixes"] = supportedRegPrefixes;
84         asyncResp->res.jsonValue["ResourceTypes"] = supportedResourceTypes;
85 
86         nlohmann::json::object_t supportedSSEFilters;
87         supportedSSEFilters["EventFormatType"] = true;
88         supportedSSEFilters["MessageId"] = true;
89         supportedSSEFilters["MetricReportDefinition"] = true;
90         supportedSSEFilters["RegistryPrefix"] = true;
91         supportedSSEFilters["OriginResource"] = false;
92         supportedSSEFilters["ResourceType"] = false;
93 
94         asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
95             std::move(supportedSSEFilters);
96         });
97 
98     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
99         .privileges(redfish::privileges::patchEventService)
100         .methods(boost::beast::http::verb::patch)(
101             [&app](const crow::Request& req,
102                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
103         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
104         {
105             return;
106         }
107         std::optional<bool> serviceEnabled;
108         std::optional<uint32_t> retryAttemps;
109         std::optional<uint32_t> retryInterval;
110 
111         if (!json_util::readJsonPatch(
112                 req, asyncResp->res, "ServiceEnabled", serviceEnabled,
113                 "DeliveryRetryAttempts", retryAttemps,
114                 "DeliveryRetryIntervalSeconds", retryInterval))
115         {
116             return;
117         }
118 
119         persistent_data::EventServiceConfig eventServiceConfig =
120             persistent_data::EventServiceStore::getInstance()
121                 .getEventServiceConfig();
122 
123         if (serviceEnabled)
124         {
125             eventServiceConfig.enabled = *serviceEnabled;
126         }
127 
128         if (retryAttemps)
129         {
130             // Supported range [1-3]
131             if ((*retryAttemps < 1) || (*retryAttemps > 3))
132             {
133                 messages::queryParameterOutOfRange(
134                     asyncResp->res, std::to_string(*retryAttemps),
135                     "DeliveryRetryAttempts", "[1-3]");
136             }
137             else
138             {
139                 eventServiceConfig.retryAttempts = *retryAttemps;
140             }
141         }
142 
143         if (retryInterval)
144         {
145             // Supported range [5 - 180]
146             if ((*retryInterval < 5) || (*retryInterval > 180))
147             {
148                 messages::queryParameterOutOfRange(
149                     asyncResp->res, std::to_string(*retryInterval),
150                     "DeliveryRetryIntervalSeconds", "[5-180]");
151             }
152             else
153             {
154                 eventServiceConfig.retryTimeoutInterval = *retryInterval;
155             }
156         }
157 
158         EventServiceManager::getInstance().setEventServiceConfig(
159             eventServiceConfig);
160         });
161 }
162 
163 inline void requestRoutesSubmitTestEvent(App& app)
164 {
165     BMCWEB_ROUTE(
166         app, "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
167         .privileges(redfish::privileges::postEventService)
168         .methods(boost::beast::http::verb::post)(
169             [&app](const crow::Request& req,
170                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
171         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
172         {
173             return;
174         }
175         if (!EventServiceManager::getInstance().sendTestEventLog())
176         {
177             messages::serviceDisabled(asyncResp->res,
178                                       "/redfish/v1/EventService/");
179             return;
180         }
181         asyncResp->res.result(boost::beast::http::status::no_content);
182         });
183 }
184 
185 inline void requestRoutesEventDestinationCollection(App& app)
186 {
187     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
188         .privileges(redfish::privileges::getEventDestinationCollection)
189         .methods(boost::beast::http::verb::get)(
190             [&app](const crow::Request& req,
191                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
192         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
193         {
194             return;
195         }
196         asyncResp->res.jsonValue["@odata.type"] =
197             "#EventDestinationCollection.EventDestinationCollection";
198         asyncResp->res.jsonValue["@odata.id"] =
199             "/redfish/v1/EventService/Subscriptions";
200         asyncResp->res.jsonValue["Name"] = "Event Destination Collections";
201 
202         nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
203 
204         std::vector<std::string> subscripIds =
205             EventServiceManager::getInstance().getAllIDs();
206         memberArray = nlohmann::json::array();
207         asyncResp->res.jsonValue["Members@odata.count"] = subscripIds.size();
208 
209         for (const std::string& id : subscripIds)
210         {
211             nlohmann::json::object_t member;
212             member["@odata.id"] = "/redfish/v1/EventService/Subscriptions/" +
213                                   id;
214             memberArray.emplace_back(std::move(member));
215         }
216         });
217     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
218         .privileges(redfish::privileges::postEventDestinationCollection)
219         .methods(boost::beast::http::verb::post)(
220             [&app](const crow::Request& req,
221                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
222         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
223         {
224             return;
225         }
226         if (EventServiceManager::getInstance().getNumberOfSubscriptions() >=
227             maxNoOfSubscriptions)
228         {
229             messages::eventSubscriptionLimitExceeded(asyncResp->res);
230             return;
231         }
232         std::string destUrl;
233         std::string protocol;
234         std::optional<std::string> context;
235         std::optional<std::string> subscriptionType;
236         std::optional<std::string> eventFormatType2;
237         std::optional<std::string> retryPolicy;
238         std::optional<std::vector<std::string>> msgIds;
239         std::optional<std::vector<std::string>> regPrefixes;
240         std::optional<std::vector<std::string>> resTypes;
241         std::optional<std::vector<nlohmann::json>> headers;
242         std::optional<std::vector<nlohmann::json>> mrdJsonArray;
243 
244         if (!json_util::readJsonPatch(
245                 req, asyncResp->res, "Destination", destUrl, "Context", context,
246                 "Protocol", protocol, "SubscriptionType", subscriptionType,
247                 "EventFormatType", eventFormatType2, "HttpHeaders", headers,
248                 "RegistryPrefixes", regPrefixes, "MessageIds", msgIds,
249                 "DeliveryRetryPolicy", retryPolicy, "MetricReportDefinitions",
250                 mrdJsonArray, "ResourceTypes", resTypes))
251         {
252             return;
253         }
254 
255         // https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
256         static constexpr const uint16_t maxDestinationSize = 2000;
257         if (destUrl.size() > maxDestinationSize)
258         {
259             messages::stringValueTooLong(asyncResp->res, "Destination",
260                                          maxDestinationSize);
261             return;
262         }
263 
264         if (regPrefixes && msgIds)
265         {
266             if (!regPrefixes->empty() && !msgIds->empty())
267             {
268                 messages::propertyValueConflict(asyncResp->res, "MessageIds",
269                                                 "RegistryPrefixes");
270                 return;
271             }
272         }
273 
274         std::string host;
275         std::string urlProto;
276         uint16_t port = 0;
277         std::string path;
278 
279         if (!crow::utility::validateAndSplitUrl(destUrl, urlProto, host, port,
280                                                 path))
281         {
282             BMCWEB_LOG_WARNING
283                 << "Failed to validate and split destination url";
284             messages::propertyValueFormatError(asyncResp->res, destUrl,
285                                                "Destination");
286             return;
287         }
288 
289         if (path.empty())
290         {
291             path = "/";
292         }
293         std::shared_ptr<Subscription> subValue = std::make_shared<Subscription>(
294             host, port, path, urlProto, app.ioContext());
295 
296         subValue->destinationUrl = destUrl;
297 
298         if (subscriptionType)
299         {
300             if (*subscriptionType != "RedfishEvent")
301             {
302                 messages::propertyValueNotInList(
303                     asyncResp->res, *subscriptionType, "SubscriptionType");
304                 return;
305             }
306             subValue->subscriptionType = *subscriptionType;
307         }
308         else
309         {
310             subValue->subscriptionType = "RedfishEvent"; // Default
311         }
312 
313         if (protocol != "Redfish")
314         {
315             messages::propertyValueNotInList(asyncResp->res, protocol,
316                                              "Protocol");
317             return;
318         }
319         subValue->protocol = protocol;
320 
321         if (eventFormatType2)
322         {
323             if (std::find(supportedEvtFormatTypes.begin(),
324                           supportedEvtFormatTypes.end(),
325                           *eventFormatType2) == supportedEvtFormatTypes.end())
326             {
327                 messages::propertyValueNotInList(
328                     asyncResp->res, *eventFormatType2, "EventFormatType");
329                 return;
330             }
331             subValue->eventFormatType = *eventFormatType2;
332         }
333         else
334         {
335             // If not specified, use default "Event"
336             subValue->eventFormatType = "Event";
337         }
338 
339         if (context)
340         {
341             // This value is selected aribitrarily.
342             constexpr const size_t maxContextSize = 256;
343             if (context->size() > maxContextSize)
344             {
345                 messages::stringValueTooLong(asyncResp->res, "Context",
346                                              maxContextSize);
347                 return;
348             }
349             subValue->customText = *context;
350         }
351 
352         if (headers)
353         {
354             size_t cumulativeLen = 0;
355 
356             for (const nlohmann::json& headerChunk : *headers)
357             {
358                 std::string hdr{headerChunk.dump(
359                     -1, ' ', true, nlohmann::json::error_handler_t::replace)};
360                 cumulativeLen += hdr.length();
361 
362                 // This value is selected to mirror http_connection.hpp
363                 constexpr const uint16_t maxHeaderSizeED = 8096;
364                 if (cumulativeLen > maxHeaderSizeED)
365                 {
366                     messages::arraySizeTooLong(asyncResp->res, "HttpHeaders",
367                                                maxHeaderSizeED);
368                     return;
369                 }
370                 for (const auto& item : headerChunk.items())
371                 {
372                     const std::string* value =
373                         item.value().get_ptr<const std::string*>();
374                     if (value == nullptr)
375                     {
376                         messages::propertyValueFormatError(
377                             asyncResp->res, item.value().dump(2, 1),
378                             "HttpHeaders/" + item.key());
379                         return;
380                     }
381                     subValue->httpHeaders.set(item.key(), *value);
382                 }
383             }
384         }
385 
386         if (regPrefixes)
387         {
388             for (const std::string& it : *regPrefixes)
389             {
390                 if (std::find(supportedRegPrefixes.begin(),
391                               supportedRegPrefixes.end(),
392                               it) == supportedRegPrefixes.end())
393                 {
394                     messages::propertyValueNotInList(asyncResp->res, it,
395                                                      "RegistryPrefixes");
396                     return;
397                 }
398             }
399             subValue->registryPrefixes = *regPrefixes;
400         }
401 
402         if (resTypes)
403         {
404             for (const std::string& it : *resTypes)
405             {
406                 if (std::find(supportedResourceTypes.begin(),
407                               supportedResourceTypes.end(),
408                               it) == supportedResourceTypes.end())
409                 {
410                     messages::propertyValueNotInList(asyncResp->res, it,
411                                                      "ResourceTypes");
412                     return;
413                 }
414             }
415             subValue->resourceTypes = *resTypes;
416         }
417 
418         if (msgIds)
419         {
420             std::vector<std::string> registryPrefix;
421 
422             // If no registry prefixes are mentioned, consider all
423             // supported prefixes
424             if (subValue->registryPrefixes.empty())
425             {
426                 registryPrefix.assign(supportedRegPrefixes.begin(),
427                                       supportedRegPrefixes.end());
428             }
429             else
430             {
431                 registryPrefix = subValue->registryPrefixes;
432             }
433 
434             for (const std::string& id : *msgIds)
435             {
436                 bool validId = false;
437 
438                 // Check for Message ID in each of the selected Registry
439                 for (const std::string& it : registryPrefix)
440                 {
441                     const std::span<const redfish::registries::MessageEntry>
442                         registry =
443                             redfish::registries::getRegistryFromPrefix(it);
444 
445                     if (std::any_of(
446                             registry.begin(), registry.end(),
447                             [&id](const redfish::registries::MessageEntry&
448                                       messageEntry) {
449                         return id == messageEntry.first;
450                             }))
451                     {
452                         validId = true;
453                         break;
454                     }
455                 }
456 
457                 if (!validId)
458                 {
459                     messages::propertyValueNotInList(asyncResp->res, id,
460                                                      "MessageIds");
461                     return;
462                 }
463             }
464 
465             subValue->registryMsgIds = *msgIds;
466         }
467 
468         if (retryPolicy)
469         {
470             if (std::find(supportedRetryPolicies.begin(),
471                           supportedRetryPolicies.end(),
472                           *retryPolicy) == supportedRetryPolicies.end())
473             {
474                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
475                                                  "DeliveryRetryPolicy");
476                 return;
477             }
478             subValue->retryPolicy = *retryPolicy;
479         }
480         else
481         {
482             // Default "TerminateAfterRetries"
483             subValue->retryPolicy = "TerminateAfterRetries";
484         }
485 
486         if (mrdJsonArray)
487         {
488             for (nlohmann::json& mrdObj : *mrdJsonArray)
489             {
490                 std::string mrdUri;
491 
492                 if (!json_util::readJson(mrdObj, asyncResp->res, "@odata.id",
493                                          mrdUri))
494 
495                 {
496                     return;
497                 }
498                 subValue->metricReportDefinitions.emplace_back(mrdUri);
499             }
500         }
501 
502         std::string id =
503             EventServiceManager::getInstance().addSubscription(subValue);
504         if (id.empty())
505         {
506             messages::internalError(asyncResp->res);
507             return;
508         }
509 
510         messages::created(asyncResp->res);
511         asyncResp->res.addHeader(
512             "Location", "/redfish/v1/EventService/Subscriptions/" + id);
513         });
514 }
515 
516 inline void requestRoutesEventDestination(App& app)
517 {
518     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
519         .privileges(redfish::privileges::getEventDestination)
520         .methods(boost::beast::http::verb::get)(
521             [&app](const crow::Request& req,
522                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
523                    const std::string& param) {
524         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
525         {
526             return;
527         }
528         std::shared_ptr<Subscription> subValue =
529             EventServiceManager::getInstance().getSubscription(param);
530         if (subValue == nullptr)
531         {
532             asyncResp->res.result(boost::beast::http::status::not_found);
533             return;
534         }
535         const std::string& id = param;
536 
537         asyncResp->res.jsonValue["@odata.type"] =
538             "#EventDestination.v1_7_0.EventDestination";
539         asyncResp->res.jsonValue["Protocol"] = "Redfish";
540         asyncResp->res.jsonValue["@odata.id"] =
541             "/redfish/v1/EventService/Subscriptions/" + id;
542         asyncResp->res.jsonValue["Id"] = id;
543         asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
544         asyncResp->res.jsonValue["Destination"] = subValue->destinationUrl;
545         asyncResp->res.jsonValue["Context"] = subValue->customText;
546         asyncResp->res.jsonValue["SubscriptionType"] =
547             subValue->subscriptionType;
548         asyncResp->res.jsonValue["HttpHeaders"] = nlohmann::json::array();
549         asyncResp->res.jsonValue["EventFormatType"] = subValue->eventFormatType;
550         asyncResp->res.jsonValue["RegistryPrefixes"] =
551             subValue->registryPrefixes;
552         asyncResp->res.jsonValue["ResourceTypes"] = subValue->resourceTypes;
553 
554         asyncResp->res.jsonValue["MessageIds"] = subValue->registryMsgIds;
555         asyncResp->res.jsonValue["DeliveryRetryPolicy"] = subValue->retryPolicy;
556 
557         nlohmann::json::array_t mrdJsonArray;
558         for (const auto& mdrUri : subValue->metricReportDefinitions)
559         {
560             nlohmann::json::object_t mdr;
561             mdr["@odata.id"] = mdrUri;
562             mrdJsonArray.emplace_back(std::move(mdr));
563         }
564         asyncResp->res.jsonValue["MetricReportDefinitions"] = mrdJsonArray;
565         });
566     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
567         // The below privilege is wrong, it should be ConfigureManager OR
568         // ConfigureSelf
569         // https://github.com/openbmc/bmcweb/issues/220
570         //.privileges(redfish::privileges::patchEventDestination)
571         .privileges({{"ConfigureManager"}})
572         .methods(boost::beast::http::verb::patch)(
573             [&app](const crow::Request& req,
574                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
575                    const std::string& param) {
576         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
577         {
578             return;
579         }
580         std::shared_ptr<Subscription> subValue =
581             EventServiceManager::getInstance().getSubscription(param);
582         if (subValue == nullptr)
583         {
584             asyncResp->res.result(boost::beast::http::status::not_found);
585             return;
586         }
587 
588         std::optional<std::string> context;
589         std::optional<std::string> retryPolicy;
590         std::optional<std::vector<nlohmann::json>> headers;
591 
592         if (!json_util::readJsonPatch(req, asyncResp->res, "Context", context,
593                                       "DeliveryRetryPolicy", retryPolicy,
594                                       "HttpHeaders", headers))
595         {
596             return;
597         }
598 
599         if (context)
600         {
601             subValue->customText = *context;
602         }
603 
604         if (headers)
605         {
606             boost::beast::http::fields fields;
607             for (const nlohmann::json& headerChunk : *headers)
608             {
609                 for (const auto& it : headerChunk.items())
610                 {
611                     const std::string* value =
612                         it.value().get_ptr<const std::string*>();
613                     if (value == nullptr)
614                     {
615                         messages::propertyValueFormatError(
616                             asyncResp->res, it.value().dump(2, ' ', true),
617                             "HttpHeaders/" + it.key());
618                         return;
619                     }
620                     fields.set(it.key(), *value);
621                 }
622             }
623             subValue->httpHeaders = fields;
624         }
625 
626         if (retryPolicy)
627         {
628             if (std::find(supportedRetryPolicies.begin(),
629                           supportedRetryPolicies.end(),
630                           *retryPolicy) == supportedRetryPolicies.end())
631             {
632                 messages::propertyValueNotInList(asyncResp->res, *retryPolicy,
633                                                  "DeliveryRetryPolicy");
634                 return;
635             }
636             subValue->retryPolicy = *retryPolicy;
637         }
638 
639         EventServiceManager::getInstance().updateSubscriptionData();
640         });
641     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/<str>/")
642         // The below privilege is wrong, it should be ConfigureManager OR
643         // ConfigureSelf
644         // https://github.com/openbmc/bmcweb/issues/220
645         //.privileges(redfish::privileges::deleteEventDestination)
646         .privileges({{"ConfigureManager"}})
647         .methods(boost::beast::http::verb::delete_)(
648             [&app](const crow::Request& req,
649                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
650                    const std::string& param) {
651         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
652         {
653             return;
654         }
655         if (!EventServiceManager::getInstance().isSubscriptionExist(param))
656         {
657             asyncResp->res.result(boost::beast::http::status::not_found);
658             return;
659         }
660         EventServiceManager::getInstance().deleteSubscription(param);
661         });
662 }
663 
664 } // namespace redfish
665