xref: /openbmc/bmcweb/redfish-core/lib/event_service.hpp (revision 90e97e1d26b78d899a543831a8051dacbbdde71a)
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 "event_service_manager.hpp"
18 
19 #include <app.hpp>
20 namespace redfish
21 {
22 
23 static constexpr const std::array<const char*, 2> supportedEvtFormatTypes = {
24     eventFormatType, metricReportFormatType};
25 static constexpr const std::array<const char*, 3> supportedRegPrefixes = {
26     "Base", "OpenBMC", "TaskEvent"};
27 static constexpr const std::array<const char*, 3> supportedRetryPolicies = {
28     "TerminateAfterRetries", "SuspendRetries", "RetryForever"};
29 
30 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
31 static constexpr const std::array<const char*, 2> supportedResourceTypes = {
32     "IBMConfigFile", "Task"};
33 #else
34 static constexpr const std::array<const char*, 1> supportedResourceTypes = {
35     "Task"};
36 #endif
37 
38 static constexpr const uint8_t maxNoOfSubscriptions = 20;
39 
40 inline void requestRoutesEventService(App& app)
41 {
42     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
43         .privileges({"Login"})
44         .methods(boost::beast::http::verb::get)(
45             [](const crow::Request&,
46                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
47                 asyncResp->res.jsonValue = {
48                     {"@odata.type", "#EventService.v1_5_0.EventService"},
49                     {"Id", "EventService"},
50                     {"Name", "Event Service"},
51                     {"Subscriptions",
52                      {{"@odata.id", "/redfish/v1/EventService/Subscriptions"}}},
53                     {"Actions",
54                      {{"#EventService.SubmitTestEvent",
55                        {{"target", "/redfish/v1/EventService/Actions/"
56                                    "EventService.SubmitTestEvent"}}}}},
57                     {"@odata.id", "/redfish/v1/EventService"}};
58 
59                 const auto& [enabled, retryAttempts, retryTimeoutInterval] =
60                     EventServiceManager::getInstance().getEventServiceConfig();
61 
62                 asyncResp->res.jsonValue["Status"]["State"] =
63                     (enabled ? "Enabled" : "Disabled");
64                 asyncResp->res.jsonValue["ServiceEnabled"] = enabled;
65                 asyncResp->res.jsonValue["DeliveryRetryAttempts"] =
66                     retryAttempts;
67                 asyncResp->res.jsonValue["DeliveryRetryIntervalSeconds"] =
68                     retryTimeoutInterval;
69                 asyncResp->res.jsonValue["EventFormatTypes"] =
70                     supportedEvtFormatTypes;
71                 asyncResp->res.jsonValue["RegistryPrefixes"] =
72                     supportedRegPrefixes;
73                 asyncResp->res.jsonValue["ResourceTypes"] =
74                     supportedResourceTypes;
75 
76                 nlohmann::json supportedSSEFilters = {
77                     {"EventFormatType", true},        {"MessageId", true},
78                     {"MetricReportDefinition", true}, {"RegistryPrefix", true},
79                     {"OriginResource", false},        {"ResourceType", false}};
80 
81                 asyncResp->res.jsonValue["SSEFilterPropertiesSupported"] =
82                     supportedSSEFilters;
83             });
84 
85     BMCWEB_ROUTE(app, "/redfish/v1/EventService/")
86         .privileges({"ConfigureManager"})
87         .methods(boost::beast::http::verb::patch)(
88             [](const crow::Request& req,
89                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
90 
91             {
92                 std::optional<bool> serviceEnabled;
93                 std::optional<uint32_t> retryAttemps;
94                 std::optional<uint32_t> retryInterval;
95 
96                 if (!json_util::readJson(
97                         req, asyncResp->res, "ServiceEnabled", serviceEnabled,
98                         "DeliveryRetryAttempts", retryAttemps,
99                         "DeliveryRetryIntervalSeconds", retryInterval))
100                 {
101                     return;
102                 }
103 
104                 auto [enabled, retryCount, retryTimeoutInterval] =
105                     EventServiceManager::getInstance().getEventServiceConfig();
106 
107                 if (serviceEnabled)
108                 {
109                     enabled = *serviceEnabled;
110                 }
111 
112                 if (retryAttemps)
113                 {
114                     // Supported range [1-3]
115                     if ((*retryAttemps < 1) || (*retryAttemps > 3))
116                     {
117                         messages::queryParameterOutOfRange(
118                             asyncResp->res, std::to_string(*retryAttemps),
119                             "DeliveryRetryAttempts", "[1-3]");
120                     }
121                     else
122                     {
123                         retryCount = *retryAttemps;
124                     }
125                 }
126 
127                 if (retryInterval)
128                 {
129                     // Supported range [30 - 180]
130                     if ((*retryInterval < 30) || (*retryInterval > 180))
131                     {
132                         messages::queryParameterOutOfRange(
133                             asyncResp->res, std::to_string(*retryInterval),
134                             "DeliveryRetryIntervalSeconds", "[30-180]");
135                     }
136                     else
137                     {
138                         retryTimeoutInterval = *retryInterval;
139                     }
140                 }
141 
142                 EventServiceManager::getInstance().setEventServiceConfig(
143                     std::make_tuple(enabled, retryCount, retryTimeoutInterval));
144             });
145 }
146 
147 inline void requestRoutesSubmitTestEvent(App& app)
148 {
149 
150     BMCWEB_ROUTE(
151         app, "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent/")
152         .privileges({"ConfigureManager"})
153         .methods(boost::beast::http::verb::post)(
154             [](const crow::Request&,
155                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
156                 EventServiceManager::getInstance().sendTestEventLog();
157                 asyncResp->res.result(boost::beast::http::status::no_content);
158             });
159 }
160 
161 inline void requestRoutesEventDestinationCollection(App& app)
162 {
163     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions")
164         .privileges({"Login"})
165         .methods(boost::beast::http::verb::get)(
166             [](const crow::Request&,
167                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
168                 asyncResp->res.jsonValue = {
169                     {"@odata.type",
170                      "#EventDestinationCollection.EventDestinationCollection"},
171                     {"@odata.id", "/redfish/v1/EventService/Subscriptions"},
172                     {"Name", "Event Destination Collections"}};
173 
174                 nlohmann::json& memberArray =
175                     asyncResp->res.jsonValue["Members"];
176 
177                 std::vector<std::string> subscripIds =
178                     EventServiceManager::getInstance().getAllIDs();
179                 memberArray = nlohmann::json::array();
180                 asyncResp->res.jsonValue["Members@odata.count"] =
181                     subscripIds.size();
182 
183                 for (const std::string& id : subscripIds)
184                 {
185                     memberArray.push_back(
186                         {{"@odata.id",
187                           "/redfish/v1/EventService/Subscriptions/" + id}});
188                 }
189             });
190     BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
191         .privileges({"ConfigureManager"})
192         .methods(boost::beast::http::verb::post)(
193             [](const crow::Request& req,
194                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
195                 if (EventServiceManager::getInstance()
196                         .getNumberOfSubscriptions() >= maxNoOfSubscriptions)
197                 {
198                     messages::eventSubscriptionLimitExceeded(asyncResp->res);
199                     return;
200                 }
201                 std::string destUrl;
202                 std::string protocol;
203                 std::optional<std::string> context;
204                 std::optional<std::string> subscriptionType;
205                 std::optional<std::string> eventFormatType2;
206                 std::optional<std::string> retryPolicy;
207                 std::optional<std::vector<std::string>> msgIds;
208                 std::optional<std::vector<std::string>> regPrefixes;
209                 std::optional<std::vector<std::string>> resTypes;
210                 std::optional<std::vector<nlohmann::json>> headers;
211                 std::optional<std::vector<nlohmann::json>> mrdJsonArray;
212 
213                 if (!json_util::readJson(
214                         req, asyncResp->res, "Destination", destUrl, "Context",
215                         context, "Protocol", protocol, "SubscriptionType",
216                         subscriptionType, "EventFormatType", eventFormatType2,
217                         "HttpHeaders", headers, "RegistryPrefixes", regPrefixes,
218                         "MessageIds", msgIds, "DeliveryRetryPolicy",
219                         retryPolicy, "MetricReportDefinitions", mrdJsonArray,
220                         "ResourceTypes", resTypes))
221                 {
222                     return;
223                 }
224 
225                 if (regPrefixes && msgIds)
226                 {
227                     if (regPrefixes->size() && msgIds->size())
228                     {
229                         messages::mutualExclusiveProperties(
230                             asyncResp->res, "RegistryPrefixes", "MessageIds");
231                         return;
232                     }
233                 }
234 
235                 // Validate the URL using regex expression
236                 // Format: <protocol>://<host>:<port>/<uri>
237                 // protocol: http/https
238                 // host: Exclude ' ', ':', '#', '?'
239                 // port: Empty or numeric value with ':' separator.
240                 // uri: Start with '/' and Exclude '#', ' '
241                 //      Can include query params(ex: '/event?test=1')
242                 // TODO: Need to validate hostname extensively(as per rfc)
243                 const std::regex urlRegex(
244                     "(http|https)://([^/\\x20\\x3f\\x23\\x3a]+):?([0-9]*)(/"
245                     "([^\\x20\\x23\\x3f]*\\x3f?([^\\x20\\x23\\x3f])*)?)");
246                 std::cmatch match;
247                 if (!std::regex_match(destUrl.c_str(), match, urlRegex))
248                 {
249                     messages::propertyValueFormatError(asyncResp->res, destUrl,
250                                                        "Destination");
251                     return;
252                 }
253 
254                 std::string uriProto =
255                     std::string(match[1].first, match[1].second);
256                 if (uriProto == "http")
257                 {
258 #ifndef BMCWEB_INSECURE_ENABLE_HTTP_PUSH_STYLE_EVENTING
259                     messages::propertyValueFormatError(asyncResp->res, destUrl,
260                                                        "Destination");
261                     return;
262 #endif
263                 }
264 
265                 std::string host = std::string(match[2].first, match[2].second);
266                 std::string port = std::string(match[3].first, match[3].second);
267                 std::string path = std::string(match[4].first, match[4].second);
268                 if (port.empty())
269                 {
270                     if (uriProto == "http")
271                     {
272                         port = "80";
273                     }
274                     else
275                     {
276                         port = "443";
277                     }
278                 }
279                 if (path.empty())
280                 {
281                     path = "/";
282                 }
283 
284                 std::shared_ptr<Subscription> subValue =
285                     std::make_shared<Subscription>(host, port, path, uriProto);
286 
287                 subValue->destinationUrl = destUrl;
288 
289                 if (subscriptionType)
290                 {
291                     if (*subscriptionType != "RedfishEvent")
292                     {
293                         messages::propertyValueNotInList(asyncResp->res,
294                                                          *subscriptionType,
295                                                          "SubscriptionType");
296                         return;
297                     }
298                     subValue->subscriptionType = *subscriptionType;
299                 }
300                 else
301                 {
302                     subValue->subscriptionType = "RedfishEvent"; // Default
303                 }
304 
305                 if (protocol != "Redfish")
306                 {
307                     messages::propertyValueNotInList(asyncResp->res, protocol,
308                                                      "Protocol");
309                     return;
310                 }
311                 subValue->protocol = protocol;
312 
313                 if (eventFormatType2)
314                 {
315                     if (std::find(supportedEvtFormatTypes.begin(),
316                                   supportedEvtFormatTypes.end(),
317                                   *eventFormatType2) ==
318                         supportedEvtFormatTypes.end())
319                     {
320                         messages::propertyValueNotInList(asyncResp->res,
321                                                          *eventFormatType2,
322                                                          "EventFormatType");
323                         return;
324                     }
325                     subValue->eventFormatType = *eventFormatType2;
326                 }
327                 else
328                 {
329                     // If not specified, use default "Event"
330                     subValue->eventFormatType = "Event";
331                 }
332 
333                 if (context)
334                 {
335                     subValue->customText = *context;
336                 }
337 
338                 if (headers)
339                 {
340                     subValue->httpHeaders = *headers;
341                 }
342 
343                 if (regPrefixes)
344                 {
345                     for (const std::string& it : *regPrefixes)
346                     {
347                         if (std::find(supportedRegPrefixes.begin(),
348                                       supportedRegPrefixes.end(),
349                                       it) == supportedRegPrefixes.end())
350                         {
351                             messages::propertyValueNotInList(
352                                 asyncResp->res, it, "RegistryPrefixes");
353                             return;
354                         }
355                     }
356                     subValue->registryPrefixes = *regPrefixes;
357                 }
358 
359                 if (resTypes)
360                 {
361                     for (const std::string& it : *resTypes)
362                     {
363                         if (std::find(supportedResourceTypes.begin(),
364                                       supportedResourceTypes.end(),
365                                       it) == supportedResourceTypes.end())
366                         {
367                             messages::propertyValueNotInList(asyncResp->res, it,
368                                                              "ResourceTypes");
369                             return;
370                         }
371                     }
372                     subValue->resourceTypes = *resTypes;
373                 }
374 
375                 if (msgIds)
376                 {
377                     std::vector<std::string> registryPrefix;
378 
379                     // If no registry prefixes are mentioned, consider all
380                     // supported prefixes
381                     if (subValue->registryPrefixes.empty())
382                     {
383                         registryPrefix.assign(supportedRegPrefixes.begin(),
384                                               supportedRegPrefixes.end());
385                     }
386                     else
387                     {
388                         registryPrefix = subValue->registryPrefixes;
389                     }
390 
391                     for (const std::string& id : *msgIds)
392                     {
393                         bool validId = false;
394 
395                         // Check for Message ID in each of the selected Registry
396                         for (const std::string& it : registryPrefix)
397                         {
398                             const boost::beast::span<
399                                 const redfish::message_registries::MessageEntry>
400                                 registry = redfish::message_registries::
401                                     getRegistryFromPrefix(it);
402 
403                             if (std::any_of(
404                                     registry.cbegin(), registry.cend(),
405                                     [&id](const redfish::message_registries::
406                                               MessageEntry& messageEntry) {
407                                         return !id.compare(messageEntry.first);
408                                     }))
409                             {
410                                 validId = true;
411                                 break;
412                             }
413                         }
414 
415                         if (!validId)
416                         {
417                             messages::propertyValueNotInList(asyncResp->res, id,
418                                                              "MessageIds");
419                             return;
420                         }
421                     }
422 
423                     subValue->registryMsgIds = *msgIds;
424                 }
425 
426                 if (retryPolicy)
427                 {
428                     if (std::find(supportedRetryPolicies.begin(),
429                                   supportedRetryPolicies.end(),
430                                   *retryPolicy) == supportedRetryPolicies.end())
431                     {
432                         messages::propertyValueNotInList(asyncResp->res,
433                                                          *retryPolicy,
434                                                          "DeliveryRetryPolicy");
435                         return;
436                     }
437                     subValue->retryPolicy = *retryPolicy;
438                 }
439                 else
440                 {
441                     // Default "TerminateAfterRetries"
442                     subValue->retryPolicy = "TerminateAfterRetries";
443                 }
444 
445                 if (mrdJsonArray)
446                 {
447                     for (nlohmann::json& mrdObj : *mrdJsonArray)
448                     {
449                         std::string mrdUri;
450                         if (json_util::getValueFromJsonObject(
451                                 mrdObj, "@odata.id", mrdUri))
452                         {
453                             subValue->metricReportDefinitions.emplace_back(
454                                 mrdUri);
455                         }
456                         else
457                         {
458                             messages::propertyValueFormatError(
459                                 asyncResp->res,
460                                 mrdObj.dump(
461                                     2, ' ', true,
462                                     nlohmann::json::error_handler_t::replace),
463                                 "MetricReportDefinitions");
464                             return;
465                         }
466                     }
467                 }
468 
469                 std::string id =
470                     EventServiceManager::getInstance().addSubscription(
471                         subValue);
472                 if (id.empty())
473                 {
474                     messages::internalError(asyncResp->res);
475                     return;
476                 }
477 
478                 messages::created(asyncResp->res);
479                 asyncResp->res.addHeader(
480                     "Location", "/redfish/v1/EventService/Subscriptions/" + id);
481             });
482 }
483 
484 inline void requestRoutesEventDestination(App& app)
485 {
486     BMCWEB_ROUTE(app, "redfish/v1/EventService/Subscriptions/<str>/")
487         .privileges({"Login"})
488         .methods(boost::beast::http::verb::get)(
489             [](const crow::Request&,
490                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
491                const std::string& param) {
492                 std::shared_ptr<Subscription> subValue =
493                     EventServiceManager::getInstance().getSubscription(param);
494                 if (subValue == nullptr)
495                 {
496                     asyncResp->res.result(
497                         boost::beast::http::status::not_found);
498                     return;
499                 }
500                 const std::string& id = param;
501 
502                 asyncResp->res.jsonValue = {
503                     {"@odata.type",
504                      "#EventDestination.v1_7_0.EventDestination"},
505                     {"Protocol", "Redfish"}};
506                 asyncResp->res.jsonValue["@odata.id"] =
507                     "/redfish/v1/EventService/Subscriptions/" + id;
508                 asyncResp->res.jsonValue["Id"] = id;
509                 asyncResp->res.jsonValue["Name"] = "Event Destination " + id;
510                 asyncResp->res.jsonValue["Destination"] =
511                     subValue->destinationUrl;
512                 asyncResp->res.jsonValue["Context"] = subValue->customText;
513                 asyncResp->res.jsonValue["SubscriptionType"] =
514                     subValue->subscriptionType;
515                 asyncResp->res.jsonValue["HttpHeaders"] = subValue->httpHeaders;
516                 asyncResp->res.jsonValue["EventFormatType"] =
517                     subValue->eventFormatType;
518                 asyncResp->res.jsonValue["RegistryPrefixes"] =
519                     subValue->registryPrefixes;
520                 asyncResp->res.jsonValue["ResourceTypes"] =
521                     subValue->resourceTypes;
522 
523                 asyncResp->res.jsonValue["MessageIds"] =
524                     subValue->registryMsgIds;
525                 asyncResp->res.jsonValue["DeliveryRetryPolicy"] =
526                     subValue->retryPolicy;
527 
528                 std::vector<nlohmann::json> mrdJsonArray;
529                 for (const auto& mdrUri : subValue->metricReportDefinitions)
530                 {
531                     mrdJsonArray.push_back({{"@odata.id", mdrUri}});
532                 }
533                 asyncResp->res.jsonValue["MetricReportDefinitions"] =
534                     mrdJsonArray;
535             });
536     /////redfish/v1/EventService/Subscriptions/
537     // ConfigureManager
538     BMCWEB_ROUTE(app, "redfish/v1/EventService/Subscriptions/<str>/")
539         .privileges({"ConfigureManager"})
540         .methods(boost::beast::http::verb::patch)(
541             [](const crow::Request& req,
542                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
543                const std::string& param) {
544                 std::shared_ptr<Subscription> subValue =
545                     EventServiceManager::getInstance().getSubscription(param);
546                 if (subValue == nullptr)
547                 {
548                     asyncResp->res.result(
549                         boost::beast::http::status::not_found);
550                     return;
551                 }
552 
553                 std::optional<std::string> context;
554                 std::optional<std::string> retryPolicy;
555                 std::optional<std::vector<nlohmann::json>> headers;
556 
557                 if (!json_util::readJson(req, asyncResp->res, "Context",
558                                          context, "DeliveryRetryPolicy",
559                                          retryPolicy, "HttpHeaders", headers))
560                 {
561                     return;
562                 }
563 
564                 if (context)
565                 {
566                     subValue->customText = *context;
567                 }
568 
569                 if (headers)
570                 {
571                     subValue->httpHeaders = *headers;
572                 }
573 
574                 if (retryPolicy)
575                 {
576                     if (std::find(supportedRetryPolicies.begin(),
577                                   supportedRetryPolicies.end(),
578                                   *retryPolicy) == supportedRetryPolicies.end())
579                     {
580                         messages::propertyValueNotInList(asyncResp->res,
581                                                          *retryPolicy,
582                                                          "DeliveryRetryPolicy");
583                         return;
584                     }
585                     subValue->retryPolicy = *retryPolicy;
586                     subValue->updateRetryPolicy();
587                 }
588 
589                 EventServiceManager::getInstance().updateSubscriptionData();
590             });
591     BMCWEB_ROUTE(app, "redfish/v1/EventService/Subscriptions/<str>/")
592         .privileges({"ConfigureManager"})
593         .methods(boost::beast::http::verb::delete_)(
594             [](const crow::Request&,
595                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
596                const std::string& param) {
597                 if (!EventServiceManager::getInstance().isSubscriptionExist(
598                         param))
599                 {
600                     asyncResp->res.result(
601                         boost::beast::http::status::not_found);
602                     return;
603                 }
604                 EventServiceManager::getInstance().deleteSubscription(param);
605             });
606 }
607 
608 } // namespace redfish
609