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